mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
feat: secure exec-server orchestrator comms
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
11
MODULE.bazel.lock
generated
11
MODULE.bazel.lock
generated
@@ -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\"]}}",
|
||||
@@ -662,8 +663,8 @@
|
||||
"autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}",
|
||||
"aws-config_1.8.12": "{\"dependencies\":[{\"features\":[\"test-util\"],\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"default_features\":false,\"name\":\"aws-sdk-signin\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"name\":\"aws-sdk-sso\",\"optional\":true,\"req\":\"^1.91.0\"},{\"default_features\":false,\"name\":\"aws-sdk-ssooidc\",\"optional\":true,\"req\":\"^1.93.0\"},{\"default_features\":false,\"name\":\"aws-sdk-sts\",\"req\":\"^1.95.0\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"rt-tokio\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"features\":[\"default-client\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-http-client\",\"req\":\"^1.1.5\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"base64-simd\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"hex\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"p256\",\"optional\":true,\"req\":\"^0.13.2\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8.5\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.9\"},{\"features\":[\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.4\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.13.1\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"fmt\",\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.4\"},{\"name\":\"url\",\"req\":\"^2.5.4\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.18.1\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"allow-compilation\":[],\"behavior-version-latest\":[],\"client-hyper\":[\"aws-smithy-runtime/default-https-client\"],\"credentials-login\":[\"dep:aws-sdk-signin\",\"dep:sha2\",\"dep:zeroize\",\"dep:hex\",\"dep:base64-simd\",\"dep:uuid\",\"uuid?/v4\",\"dep:p256\",\"p256?/arithmetic\",\"p256?/pem\",\"dep:rand\"],\"credentials-process\":[\"tokio/process\"],\"default\":[\"default-https-client\",\"rt-tokio\",\"credentials-process\",\"sso\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-runtime/rt-tokio\",\"tokio/rt\"],\"rustls\":[\"client-hyper\"],\"sso\":[\"dep:aws-sdk-sso\",\"dep:aws-sdk-ssooidc\",\"dep:ring\",\"dep:hex\",\"dep:zeroize\",\"aws-smithy-runtime-api/http-auth\"],\"test-util\":[\"aws-runtime/test-util\"]}}",
|
||||
"aws-credential-types_1.2.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1.74\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"client\",\"http-auth\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"full\",\"test-util\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"zeroize\",\"req\":\"^1.7.0\"}],\"features\":{\"hardcoded-credentials\":[],\"test-util\":[\"aws-smithy-runtime-api/test-util\"]}}",
|
||||
"aws-lc-rs_1.16.2": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.39.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}",
|
||||
"aws-lc-sys_0.39.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}",
|
||||
"aws-lc-rs_1.17.0": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.41.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"legacy-des\":[\"aws-lc-sys?/all-bindings\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}",
|
||||
"aws-lc-sys_0.41.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}",
|
||||
"aws-runtime_1.5.17": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.3\"},{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"http0-compat\"],\"name\":\"aws-sigv4\",\"req\":\"^1.3.7\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-eventstream\",\"optional\":true,\"req\":\"^0.60.14\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"kind\":\"dev\",\"name\":\"aws-smithy-protocol-test\",\"req\":\"^0.63.7\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.10.0\"},{\"kind\":\"dev\",\"name\":\"bytes-utils\",\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"convert_case\",\"req\":\"^0.6.0\"},{\"name\":\"fastrand\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.29\"},{\"name\":\"http-02x\",\"package\":\"http\",\"req\":\"^0.2.9\"},{\"name\":\"http-1x\",\"optional\":true,\"package\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body-04x\",\"package\":\"http-body\",\"req\":\"^0.4.5\"},{\"name\":\"http-body-1x\",\"optional\":true,\"package\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2\"},{\"name\":\"regex-lite\",\"optional\":true,\"req\":\"^0.1.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2.4\"},{\"name\":\"uuid\",\"req\":\"^1\"}],\"features\":{\"event-stream\":[\"dep:aws-smithy-eventstream\",\"aws-sigv4/sign-eventstream\"],\"http-02x\":[],\"http-1x\":[\"dep:http-1x\",\"dep:http-body-1x\"],\"sigv4a\":[\"aws-sigv4/sigv4a\"],\"test-util\":[\"dep:regex-lite\"]}}",
|
||||
"aws-sdk-signin_1.2.0": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.4.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"features\":[\"macros\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"behavior-version-latest\":[],\"default\":[\"rustls\",\"default-https-client\",\"rt-tokio\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"gated-tests\":[],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-types/rt-tokio\"],\"rustls\":[\"aws-smithy-runtime/tls-rustls\"],\"test-util\":[\"aws-credential-types/test-util\",\"aws-smithy-runtime/test-util\"]}}",
|
||||
"aws-sdk-sso_1.91.0": "{\"dependencies\":[{\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"features\":[\"test-util\"],\"kind\":\"dev\",\"name\":\"aws-credential-types\",\"req\":\"^1.2.11\"},{\"name\":\"aws-runtime\",\"req\":\"^1.5.17\"},{\"name\":\"aws-smithy-async\",\"req\":\"^1.2.7\"},{\"name\":\"aws-smithy-http\",\"req\":\"^0.62.6\"},{\"name\":\"aws-smithy-json\",\"req\":\"^0.61.8\"},{\"features\":[\"client\"],\"name\":\"aws-smithy-runtime\",\"req\":\"^1.9.5\"},{\"features\":[\"client\",\"http-02x\"],\"name\":\"aws-smithy-runtime-api\",\"req\":\"^1.9.3\"},{\"name\":\"aws-smithy-types\",\"req\":\"^1.3.5\"},{\"name\":\"aws-types\",\"req\":\"^1.3.11\"},{\"name\":\"bytes\",\"req\":\"^1.4.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"http\",\"req\":\"^0.2.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"features\":[\"macros\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"behavior-version-latest\":[],\"default\":[\"rustls\",\"default-https-client\",\"rt-tokio\"],\"default-https-client\":[\"aws-smithy-runtime/default-https-client\"],\"gated-tests\":[],\"rt-tokio\":[\"aws-smithy-async/rt-tokio\",\"aws-smithy-types/rt-tokio\"],\"rustls\":[\"aws-smithy-runtime/tls-rustls\"],\"test-util\":[\"aws-credential-types/test-util\",\"aws-smithy-runtime/test-util\"]}}",
|
||||
@@ -756,6 +757,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\":{}}",
|
||||
@@ -814,6 +816,7 @@
|
||||
"ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}",
|
||||
"ctor_0.1.26": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^1.0.98\"}],\"features\":{}}",
|
||||
"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\"]}}",
|
||||
"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\":[]}}",
|
||||
@@ -963,6 +966,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\"]}}",
|
||||
@@ -1338,6 +1342,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\":[]}}",
|
||||
@@ -1604,8 +1609,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\":[]}}",
|
||||
|
||||
102
codex-rs/Cargo.lock
generated
102
codex-rs/Cargo.lock
generated
@@ -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"
|
||||
@@ -468,6 +482,9 @@ name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
@@ -799,9 +816,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.2"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"untrusted 0.7.1",
|
||||
@@ -810,9 +827,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.39.0"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
|
||||
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
@@ -1748,6 +1765,23 @@ 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",
|
||||
"rand_core 0.6.4",
|
||||
"sha2 0.10.9",
|
||||
"thiserror-no-std",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
@@ -2768,9 +2802,11 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"aws-lc-rs",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"clatter",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
@@ -2798,6 +2834,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
@@ -4759,6 +4796,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"
|
||||
@@ -6190,6 +6236,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"
|
||||
@@ -10066,6 +10122,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"
|
||||
@@ -10298,7 +10366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.11.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -10318,7 +10386,7 @@ version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -11452,7 +11520,7 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13126,6 +13194,26 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -258,6 +258,7 @@ async-stream = "0.3.6"
|
||||
async-trait = "0.1.89"
|
||||
aws-config = "1"
|
||||
aws-credential-types = "1"
|
||||
aws-lc-rs = { version = "1.16.3", default-features = false, features = ["non-fips"] }
|
||||
aws-sigv4 = "1"
|
||||
aws-types = "1"
|
||||
axum = { version = "0.8", default-features = false }
|
||||
@@ -268,6 +269,13 @@ 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-sha",
|
||||
] }
|
||||
color-eyre = "0.6.3"
|
||||
constant_time_eq = "0.3.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
|
||||
@@ -45,7 +45,14 @@ 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::SecureChannelIdentity;
|
||||
pub use codex_exec_server::SecureChannelPublicKey;
|
||||
pub use codex_exec_server::SecureRendezvousConnectArgs;
|
||||
pub use codex_exec_server::SecureRendezvousConnectBundle;
|
||||
pub use codex_exec_server::SecureRendezvousConnectProvider;
|
||||
pub use codex_exec_server::SharedSecureRendezvousConnectProvider;
|
||||
pub use codex_extension_api::empty_extension_registry;
|
||||
pub use codex_features::Feature;
|
||||
pub use codex_features::Features;
|
||||
|
||||
@@ -13,9 +13,11 @@ workspace = true
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
aws-lc-rs = { 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 }
|
||||
@@ -46,6 +48,7 @@ tokio = { workspace = true, features = [
|
||||
tokio-util = { workspace = true, features = ["rt"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
90
codex-rs/exec-server/src/aws_lc_ml_kem.rs
Normal file
90
codex-rs/exec-server/src/aws_lc_ml_kem.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use aws_lc_rs::kem::Ciphertext;
|
||||
use aws_lc_rs::kem::DecapsulationKey;
|
||||
use aws_lc_rs::kem::EncapsulationKey;
|
||||
use aws_lc_rs::kem::ML_KEM_768;
|
||||
use clatter::KeyPair;
|
||||
use clatter::bytearray::ByteArray;
|
||||
use clatter::bytearray::HeapArray;
|
||||
use clatter::bytearray::SensitiveByteArray;
|
||||
use clatter::error::KemError;
|
||||
use clatter::error::KemResult;
|
||||
use clatter::traits::CryptoComponent;
|
||||
use clatter::traits::Kem;
|
||||
use clatter::traits::Rng;
|
||||
|
||||
pub(super) const PUBLIC_KEY_LEN: usize = 1184;
|
||||
const SECRET_KEY_LEN: usize = 2400;
|
||||
const CIPHERTEXT_LEN: usize = 1088;
|
||||
const SHARED_SECRET_LEN: usize = 32;
|
||||
|
||||
/// ML-KEM-768 implementation backed by AWS-LC through `aws-lc-rs`.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AwsLcMlKem768;
|
||||
|
||||
impl CryptoComponent for AwsLcMlKem768 {
|
||||
fn name() -> &'static str {
|
||||
"MLKEM768"
|
||||
}
|
||||
}
|
||||
|
||||
impl Kem for AwsLcMlKem768 {
|
||||
type SecretKey = SensitiveByteArray<HeapArray<SECRET_KEY_LEN>>;
|
||||
type PubKey = HeapArray<PUBLIC_KEY_LEN>;
|
||||
type Ct = HeapArray<CIPHERTEXT_LEN>;
|
||||
type Ss = SensitiveByteArray<[u8; SHARED_SECRET_LEN]>;
|
||||
|
||||
fn genkey_rng<R: Rng>(_rng: &mut R) -> KemResult<KeyPair<Self::PubKey, Self::SecretKey>> {
|
||||
// AWS-LC owns ML-KEM key-generation randomness internally, so
|
||||
// Clatter's injectable RNG cannot be plumbed through this provider.
|
||||
let decapsulation_key =
|
||||
DecapsulationKey::generate(&ML_KEM_768).map_err(|_| KemError::KeyGeneration)?;
|
||||
let encapsulation_key = decapsulation_key
|
||||
.encapsulation_key()
|
||||
.map_err(|_| KemError::KeyGeneration)?;
|
||||
let public = encapsulation_key
|
||||
.key_bytes()
|
||||
.map_err(|_| KemError::KeyGeneration)?;
|
||||
let secret = decapsulation_key
|
||||
.key_bytes()
|
||||
.map_err(|_| KemError::KeyGeneration)?;
|
||||
|
||||
Ok(KeyPair {
|
||||
public: Self::PubKey::from_slice(public.as_ref()),
|
||||
secret: Self::SecretKey::from_slice(secret.as_ref()),
|
||||
})
|
||||
}
|
||||
|
||||
fn encapsulate<R: Rng>(pk: &[u8], _rng: &mut R) -> KemResult<(Self::Ct, Self::Ss)> {
|
||||
let encapsulation_key =
|
||||
EncapsulationKey::new(&ML_KEM_768, pk).map_err(|_| KemError::Input)?;
|
||||
let (ciphertext, shared_secret) = encapsulation_key
|
||||
.encapsulate()
|
||||
.map_err(|_| KemError::Encapsulation)?;
|
||||
|
||||
Ok((
|
||||
Self::Ct::from_slice(ciphertext.as_ref()),
|
||||
Self::Ss::from_slice(shared_secret.as_ref()),
|
||||
))
|
||||
}
|
||||
|
||||
fn decapsulate(ct: &[u8], sk: &[u8]) -> KemResult<Self::Ss> {
|
||||
// Reject the length before constructing AWS-LC's ciphertext wrapper.
|
||||
// This keeps malformed wire input classified as an input error rather
|
||||
// than relying on provider-specific decapsulation behavior.
|
||||
if ct.len() != CIPHERTEXT_LEN {
|
||||
return Err(KemError::Input);
|
||||
}
|
||||
|
||||
let decapsulation_key =
|
||||
DecapsulationKey::new(&ML_KEM_768, sk).map_err(|_| KemError::Input)?;
|
||||
let shared_secret = decapsulation_key
|
||||
.decapsulate(Ciphertext::from(ct))
|
||||
.map_err(|_| KemError::Decapsulation)?;
|
||||
|
||||
Ok(Self::Ss::from_slice(shared_secret.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "aws_lc_ml_kem_tests.rs"]
|
||||
mod tests;
|
||||
31
codex-rs/exec-server/src/aws_lc_ml_kem_tests.rs
Normal file
31
codex-rs/exec-server/src/aws_lc_ml_kem_tests.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use clatter::bytearray::ByteArray;
|
||||
use clatter::traits::Kem;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::AwsLcMlKem768;
|
||||
|
||||
#[test]
|
||||
fn kem_roundtrip() {
|
||||
let keypair = AwsLcMlKem768::genkey().expect("generate keypair");
|
||||
let mut rng = clatter::crypto::rng::DefaultRng;
|
||||
let (ciphertext, encapsulated_secret) =
|
||||
AwsLcMlKem768::encapsulate(keypair.public.as_slice(), &mut rng).expect("encapsulate");
|
||||
let decapsulated_secret =
|
||||
AwsLcMlKem768::decapsulate(ciphertext.as_slice(), keypair.secret.as_slice())
|
||||
.expect("decapsulate");
|
||||
|
||||
assert_eq!(
|
||||
encapsulated_secret.as_slice(),
|
||||
decapsulated_secret.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decapsulate_rejects_wrong_ciphertext_length() {
|
||||
let keypair = AwsLcMlKem768::genkey().expect("generate keypair");
|
||||
|
||||
let error = AwsLcMlKem768::decapsulate(&[], keypair.secret.as_slice())
|
||||
.expect_err("empty ciphertext should be rejected");
|
||||
|
||||
assert!(matches!(error, clatter::error::KemError::Input));
|
||||
}
|
||||
@@ -24,6 +24,8 @@ use crate::client_api::ExecServerClientConnectOptions;
|
||||
use crate::client_api::ExecServerTransportParams;
|
||||
use crate::client_api::HttpClient;
|
||||
use crate::client_api::RemoteExecServerConnectArgs;
|
||||
use crate::client_api::SecureRendezvousConnectArgs;
|
||||
use crate::client_api::SecureRendezvousConnectBundle;
|
||||
use crate::client_api::StdioExecServerConnectArgs;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::process::ExecProcessEvent;
|
||||
@@ -115,6 +117,16 @@ impl From<RemoteExecServerConnectArgs> for ExecServerClientConnectOptions {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecureRendezvousConnectArgs> for ExecServerClientConnectOptions {
|
||||
fn from(value: SecureRendezvousConnectArgs) -> Self {
|
||||
Self {
|
||||
client_name: value.client_name,
|
||||
initialize_timeout: value.initialize_timeout,
|
||||
resume_session_id: value.resume_session_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdioExecServerConnectArgs> for ExecServerClientConnectOptions {
|
||||
fn from(value: StdioExecServerConnectArgs) -> Self {
|
||||
Self {
|
||||
@@ -137,6 +149,23 @@ impl RemoteExecServerConnectArgs {
|
||||
}
|
||||
}
|
||||
|
||||
impl SecureRendezvousConnectArgs {
|
||||
pub fn new(
|
||||
bundle: SecureRendezvousConnectBundle,
|
||||
harness_identity: crate::SecureChannelIdentity,
|
||||
client_name: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
bundle,
|
||||
harness_identity,
|
||||
client_name,
|
||||
connect_timeout: CONNECT_TIMEOUT,
|
||||
initialize_timeout: INITIALIZE_TIMEOUT,
|
||||
resume_session_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SessionState {
|
||||
wake_tx: watch::Sender<u64>,
|
||||
events: ExecProcessEventLog,
|
||||
@@ -231,6 +260,7 @@ impl LazyRemoteExecServerClient {
|
||||
if matches!(
|
||||
&self.transport_params,
|
||||
ExecServerTransportParams::WebSocketUrl { .. }
|
||||
| ExecServerTransportParams::SecureRendezvous { .. }
|
||||
) =>
|
||||
{
|
||||
ExecServerClient::connect_for_transport(self.transport_params.clone()).await?
|
||||
@@ -317,6 +347,8 @@ pub enum ExecServerError {
|
||||
EnvironmentRegistryAuth(String),
|
||||
#[error("environment registry request failed: {0}")]
|
||||
EnvironmentRegistryRequest(#[from] reqwest::Error),
|
||||
#[error(transparent)]
|
||||
SecureChannel(#[from] crate::SecureChannelError),
|
||||
}
|
||||
|
||||
impl ExecServerClient {
|
||||
|
||||
@@ -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::SecureChannelIdentity;
|
||||
use crate::SecureChannelPublicKey;
|
||||
|
||||
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,69 @@ pub struct RemoteExecServerConnectArgs {
|
||||
pub resume_session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Registry-authorized material for one secure rendezvous connection attempt.
|
||||
#[derive(Clone)]
|
||||
pub struct SecureRendezvousConnectBundle {
|
||||
pub websocket_url: String,
|
||||
pub environment_id: String,
|
||||
pub executor_registration_id: String,
|
||||
pub executor_public_key: SecureChannelPublicKey,
|
||||
pub harness_key_authorization: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecureRendezvousConnectBundle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecureRendezvousConnectBundle")
|
||||
.field(
|
||||
"websocket_url",
|
||||
&redacted_websocket_url(&self.websocket_url),
|
||||
)
|
||||
.field("environment_id", &self.environment_id)
|
||||
.field("executor_registration_id", &self.executor_registration_id)
|
||||
.field("executor_public_key", &self.executor_public_key)
|
||||
.field("harness_key_authorization", &"<redacted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection arguments for an authenticated secure rendezvous exec-server.
|
||||
#[derive(Clone)]
|
||||
pub struct SecureRendezvousConnectArgs {
|
||||
pub bundle: SecureRendezvousConnectBundle,
|
||||
pub harness_identity: SecureChannelIdentity,
|
||||
pub client_name: String,
|
||||
pub connect_timeout: Duration,
|
||||
pub initialize_timeout: Duration,
|
||||
pub resume_session_id: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecureRendezvousConnectArgs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecureRendezvousConnectArgs")
|
||||
.field("bundle", &self.bundle)
|
||||
.field("harness_identity", &"<redacted>")
|
||||
.field("client_name", &self.client_name)
|
||||
.field("connect_timeout", &self.connect_timeout)
|
||||
.field("initialize_timeout", &self.initialize_timeout)
|
||||
.field("resume_session_id", &self.resume_session_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Supplies fresh registry-authorized material for secure rendezvous connections.
|
||||
///
|
||||
/// Implementations must preserve one endpoint-local harness identity while
|
||||
/// refreshing short-lived registry material for every physical connection attempt.
|
||||
pub trait SecureRendezvousConnectProvider: Send + Sync {
|
||||
/// Environment ID this provider is authorized to connect to.
|
||||
fn environment_id(&self) -> &str;
|
||||
|
||||
/// Returns a fresh atomic bundle for one physical connection attempt.
|
||||
fn connect_args(&self) -> BoxFuture<'_, Result<SecureRendezvousConnectArgs, ExecServerError>>;
|
||||
}
|
||||
|
||||
pub type SharedSecureRendezvousConnectProvider = Arc<dyn SecureRendezvousConnectProvider>;
|
||||
|
||||
/// Stdio connection arguments for a command-backed exec-server.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct StdioExecServerConnectArgs {
|
||||
@@ -49,13 +115,16 @@ 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,
|
||||
},
|
||||
SecureRendezvous {
|
||||
provider: SharedSecureRendezvousConnectProvider,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
StdioCommand {
|
||||
command: StdioExecServerCommand,
|
||||
@@ -63,6 +132,35 @@ 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::SecureRendezvous { provider } => f
|
||||
.debug_struct("SecureRendezvous")
|
||||
.field("environment_id", &provider.environment_id())
|
||||
.finish(),
|
||||
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 {
|
||||
@@ -73,6 +171,17 @@ impl ExecServerTransportParams {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn redacted_websocket_url(websocket_url: &str) -> String {
|
||||
match url::Url::parse(websocket_url) {
|
||||
Ok(mut url) => {
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
url.to_string()
|
||||
}
|
||||
Err(_) => "<redacted websocket url>".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends HTTP requests through a runtime-selected transport.
|
||||
///
|
||||
/// This is the HTTP capability counterpart to [`crate::ExecBackend`]. Callers
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,10 +13,14 @@ use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use crate::ExecServerClient;
|
||||
use crate::ExecServerError;
|
||||
use crate::client_api::RemoteExecServerConnectArgs;
|
||||
use crate::client_api::SecureRendezvousConnectArgs;
|
||||
use crate::client_api::StdioExecServerCommand;
|
||||
use crate::client_api::StdioExecServerConnectArgs;
|
||||
use crate::client_api::redacted_websocket_url;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::relay::harness_connection_from_websocket;
|
||||
use crate::secure_relay::secure_harness_connection_from_websocket;
|
||||
use crate::secure_relay::secure_relay_websocket_config;
|
||||
|
||||
const ENVIRONMENT_CLIENT_NAME: &str = "codex-environment";
|
||||
|
||||
@@ -38,6 +43,16 @@ impl ExecServerClient {
|
||||
})
|
||||
.await
|
||||
}
|
||||
crate::client_api::ExecServerTransportParams::SecureRendezvous { provider } => {
|
||||
let args = provider.connect_args().await?;
|
||||
if args.bundle.environment_id != provider.environment_id() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure rendezvous provider returned a different environment id"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Self::connect_secure_rendezvous(args).await
|
||||
}
|
||||
crate::client_api::ExecServerTransportParams::StdioCommand {
|
||||
command,
|
||||
initialize_timeout,
|
||||
@@ -79,6 +94,44 @@ impl ExecServerClient {
|
||||
Self::connect(connection, args.into()).await
|
||||
}
|
||||
|
||||
pub async fn connect_secure_rendezvous(
|
||||
args: SecureRendezvousConnectArgs,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
ensure_rustls_crypto_provider();
|
||||
let websocket_url = args.bundle.websocket_url.clone();
|
||||
let diagnostic_url = redacted_websocket_url(&websocket_url);
|
||||
let connect_timeout = args.connect_timeout;
|
||||
let (stream, _) = timeout(
|
||||
connect_timeout,
|
||||
connect_async_with_config(
|
||||
websocket_url.as_str(),
|
||||
Some(secure_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!("secure exec-server rendezvous websocket {diagnostic_url}");
|
||||
let connection = secure_harness_connection_from_websocket(
|
||||
stream,
|
||||
connection_label,
|
||||
args.bundle.environment_id.clone(),
|
||||
args.bundle.executor_registration_id.clone(),
|
||||
args.harness_identity.clone(),
|
||||
args.bundle.executor_public_key.clone(),
|
||||
args.bundle.harness_key_authorization.clone(),
|
||||
);
|
||||
Self::connect(connection, args.into()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_stdio_command(
|
||||
args: StdioExecServerConnectArgs,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) enum JsonRpcConnectionEvent {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum JsonRpcTransport {
|
||||
Plain,
|
||||
External,
|
||||
Stdio { transport: StdioTransport },
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl JsonRpcTransport {
|
||||
|
||||
pub(crate) fn terminate(&self) {
|
||||
match self {
|
||||
Self::Plain => {}
|
||||
Self::External => {}
|
||||
Self::Stdio { transport } => transport.terminate(),
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ impl JsonRpcConnection {
|
||||
incoming_rx,
|
||||
disconnected_rx,
|
||||
task_handles: vec![reader_task, writer_task],
|
||||
transport: JsonRpcTransport::Plain,
|
||||
transport: JsonRpcTransport::External,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ impl JsonRpcConnection {
|
||||
incoming_rx,
|
||||
disconnected_rx,
|
||||
task_handles: vec![websocket_task],
|
||||
transport: JsonRpcTransport::Plain,
|
||||
transport: JsonRpcTransport::External,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
use crate::HttpClient;
|
||||
use crate::SharedSecureRendezvousConnectProvider;
|
||||
use crate::client::LazyRemoteExecServerClient;
|
||||
use crate::client::http_client::ReqwestHttpClient;
|
||||
use crate::client_api::ExecServerTransportParams;
|
||||
@@ -276,6 +277,34 @@ 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.
|
||||
pub fn upsert_secure_environment(
|
||||
&self,
|
||||
environment_id: String,
|
||||
provider: SharedSecureRendezvousConnectProvider,
|
||||
) -> Result<(), ExecServerError> {
|
||||
if environment_id.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"environment id cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if environment_id != provider.environment_id() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure environment id does not match connection provider".to_string(),
|
||||
));
|
||||
}
|
||||
let environment = Environment::remote_with_transport(
|
||||
ExecServerTransportParams::SecureRendezvous { provider },
|
||||
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.
|
||||
@@ -382,6 +411,7 @@ impl Environment {
|
||||
websocket_url: exec_server_url,
|
||||
..
|
||||
} => Some(exec_server_url.clone()),
|
||||
ExecServerTransportParams::SecureRendezvous { .. } => None,
|
||||
ExecServerTransportParams::StdioCommand { .. } => None,
|
||||
};
|
||||
let client = LazyRemoteExecServerClient::new(remote_transport.clone());
|
||||
|
||||
@@ -48,7 +48,7 @@ struct EnvironmentToml {
|
||||
initialize_timeout_sec: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct TomlEnvironmentProvider {
|
||||
default: EnvironmentDefault,
|
||||
include_local: bool,
|
||||
@@ -577,18 +577,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]
|
||||
@@ -614,26 +622,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]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod aws_lc_ml_kem;
|
||||
mod client;
|
||||
mod client_api;
|
||||
mod client_transport;
|
||||
@@ -22,6 +23,8 @@ mod remote_process;
|
||||
mod rpc;
|
||||
mod runtime_paths;
|
||||
mod sandboxed_file_system;
|
||||
mod secure_channel;
|
||||
mod secure_relay;
|
||||
mod server;
|
||||
|
||||
pub use client::ExecServerClient;
|
||||
@@ -31,6 +34,10 @@ pub use client::http_client::ReqwestHttpClient;
|
||||
pub use client_api::ExecServerClientConnectOptions;
|
||||
pub use client_api::HttpClient;
|
||||
pub use client_api::RemoteExecServerConnectArgs;
|
||||
pub use client_api::SecureRendezvousConnectArgs;
|
||||
pub use client_api::SecureRendezvousConnectBundle;
|
||||
pub use client_api::SecureRendezvousConnectProvider;
|
||||
pub use client_api::SharedSecureRendezvousConnectProvider;
|
||||
pub use codex_file_system::CopyOptions;
|
||||
pub use codex_file_system::CreateDirectoryOptions;
|
||||
pub use codex_file_system::ExecutorFileSystem;
|
||||
@@ -102,6 +109,9 @@ pub use protocol::WriteStatus;
|
||||
pub use remote::RemoteEnvironmentConfig;
|
||||
pub use remote::run_remote_environment;
|
||||
pub use runtime_paths::ExecServerRuntimePaths;
|
||||
pub use secure_channel::SecureChannelError;
|
||||
pub use secure_channel::SecureChannelIdentity;
|
||||
pub use secure_channel::SecureChannelPublicKey;
|
||||
pub use server::DEFAULT_LISTEN_URL;
|
||||
pub use server::ExecServerListenUrlParseError;
|
||||
pub use server::run_main;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<relay_message_frame::Body>,
|
||||
}
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use futures::Sink;
|
||||
use futures::SinkExt;
|
||||
use futures::Stream;
|
||||
use futures::StreamExt;
|
||||
use prost::Message as ProstMessage;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -22,24 +16,28 @@ use crate::connection::JsonRpcConnection;
|
||||
use crate::connection::JsonRpcConnectionEvent;
|
||||
use crate::connection::JsonRpcTransport;
|
||||
use crate::relay_proto::RelayData;
|
||||
use crate::relay_proto::RelayHandshake;
|
||||
use crate::relay_proto::RelayMessageFrame;
|
||||
use crate::relay_proto::RelayReset;
|
||||
use crate::relay_proto::RelayResume;
|
||||
use crate::relay_proto::relay_message_frame;
|
||||
use crate::server::ConnectionProcessor;
|
||||
|
||||
const RELAY_MESSAGE_FRAME_VERSION: u32 = 1;
|
||||
const MAX_RELAY_RESET_REASON_BYTES: usize = 256;
|
||||
const MAX_RELAY_STREAM_ID_BYTES: usize = 128;
|
||||
|
||||
#[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<u8>) -> Self {
|
||||
pub(crate) fn data(stream_id: String, seq: u32, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
version: RELAY_MESSAGE_FRAME_VERSION,
|
||||
stream_id,
|
||||
@@ -54,7 +52,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,
|
||||
@@ -66,18 +64,55 @@ impl RelayMessageFrame {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<RelayFrameBodyKind, ExecServerError> {
|
||||
pub(crate) fn handshake(stream_id: String, payload: Vec<u8>) -> 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 reset(stream_id: String, reason: String) -> Self {
|
||||
Self {
|
||||
version: RELAY_MESSAGE_FRAME_VERSION,
|
||||
stream_id,
|
||||
ack: 0,
|
||||
ack_bits: 0,
|
||||
body: Some(relay_message_frame::Body::Reset(RelayReset { reason })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate cleartext routing metadata before a frame reaches either the
|
||||
/// direct JSON-RPC parser or the secure relay state machine.
|
||||
///
|
||||
/// The encrypted path intentionally leaves this metadata visible to
|
||||
/// rendezvous, so it must be canonical and tightly bounded everywhere.
|
||||
pub(crate) fn validate(&self) -> Result<RelayFrameBodyKind, ExecServerError> {
|
||||
if self.version != RELAY_MESSAGE_FRAME_VERSION {
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"unsupported relay message frame version {}",
|
||||
self.version
|
||||
)));
|
||||
}
|
||||
if self.stream_id.trim().is_empty() {
|
||||
if self.stream_id.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"relay message frame is missing stream_id".to_string(),
|
||||
));
|
||||
}
|
||||
if self.stream_id.trim() != self.stream_id {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"relay message frame stream_id is not canonical".to_string(),
|
||||
));
|
||||
}
|
||||
if self.stream_id.len() > MAX_RELAY_STREAM_ID_BYTES {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"relay message frame stream_id is too long".to_string(),
|
||||
));
|
||||
}
|
||||
match self.body.as_ref() {
|
||||
Some(relay_message_frame::Body::Data(data)) => {
|
||||
if data.segment_index != 0 || data.segment_count != 1 || data.payload.is_empty() {
|
||||
@@ -90,35 +125,64 @@ impl RelayMessageFrame {
|
||||
Some(relay_message_frame::Body::AckFrame(_)) => Ok(RelayFrameBodyKind::Ack),
|
||||
Some(relay_message_frame::Body::Resume(_)) => Ok(RelayFrameBodyKind::Resume),
|
||||
Some(relay_message_frame::Body::Reset(reset)) => {
|
||||
if reset.reason.is_empty() {
|
||||
if reset.reason.is_empty() || reset.reason.len() > MAX_RELAY_RESET_REASON_BYTES {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"relay reset message frame is missing reason".to_string(),
|
||||
"relay reset message frame has invalid reason".to_string(),
|
||||
));
|
||||
}
|
||||
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<JSONRPCMessage, ExecServerError> {
|
||||
pub(crate) fn into_data(self) -> Result<RelayData, ExecServerError> {
|
||||
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<JSONRPCMessage, ExecServerError> {
|
||||
let payload = self.into_data()?.payload;
|
||||
serde_json::from_slice(&payload).map_err(ExecServerError::Json)
|
||||
}
|
||||
|
||||
fn into_reset_reason(self) -> Option<String> {
|
||||
pub(crate) fn into_handshake_payload(self) -> Result<Vec<u8>, 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<String> {
|
||||
match self.body {
|
||||
Some(relay_message_frame::Body::Reset(reset)) if !reset.reason.is_empty() => {
|
||||
Some(reset.reason)
|
||||
@@ -128,16 +192,18 @@ impl RelayMessageFrame {
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_relay_message_frame(frame: &RelayMessageFrame) -> Vec<u8> {
|
||||
pub(crate) fn encode_relay_message_frame(frame: &RelayMessageFrame) -> Vec<u8> {
|
||||
frame.encode_to_vec()
|
||||
}
|
||||
|
||||
fn decode_relay_message_frame(payload: &[u8]) -> Result<RelayMessageFrame, ExecServerError> {
|
||||
pub(crate) fn decode_relay_message_frame(
|
||||
payload: &[u8],
|
||||
) -> Result<RelayMessageFrame, ExecServerError> {
|
||||
RelayMessageFrame::decode(payload)
|
||||
.map_err(|err| ExecServerError::Protocol(format!("invalid relay message frame: {err}")))
|
||||
}
|
||||
|
||||
fn jsonrpc_payload(message: &JSONRPCMessage) -> Result<Vec<u8>, ExecServerError> {
|
||||
pub(crate) fn jsonrpc_payload(message: &JSONRPCMessage) -> Result<Vec<u8>, ExecServerError> {
|
||||
serde_json::to_vec(message).map_err(ExecServerError::Json)
|
||||
}
|
||||
|
||||
@@ -253,7 +319,8 @@ where
|
||||
}
|
||||
RelayFrameBodyKind::Ack
|
||||
| RelayFrameBodyKind::Resume
|
||||
| RelayFrameBodyKind::Heartbeat => {}
|
||||
| RelayFrameBodyKind::Heartbeat
|
||||
| RelayFrameBodyKind::Handshake => {}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => {
|
||||
@@ -294,167 +361,7 @@ where
|
||||
incoming_rx,
|
||||
disconnected_rx,
|
||||
task_handles: vec![websocket_task],
|
||||
transport: JsonRpcTransport::Plain,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run_multiplexed_environment<S>(
|
||||
stream: WebSocketStream<S>,
|
||||
processor: ConnectionProcessor,
|
||||
) where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let mut websocket = stream;
|
||||
let (physical_outgoing_tx, mut physical_outgoing_rx) =
|
||||
mpsc::channel::<Vec<u8>>(CHANNEL_CAPACITY);
|
||||
|
||||
let mut streams: HashMap<String, VirtualStream> = HashMap::new();
|
||||
loop {
|
||||
let frame = tokio::select! {
|
||||
maybe_encoded = physical_outgoing_rx.recv() => {
|
||||
let Some(encoded) = maybe_encoded else {
|
||||
break;
|
||||
};
|
||||
if websocket.send(Message::Binary(encoded.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
incoming_message = websocket.next() => match incoming_message {
|
||||
Some(Ok(Message::Binary(payload))) => {
|
||||
match decode_relay_message_frame(payload.as_ref()) {
|
||||
Ok(frame) => frame,
|
||||
Err(err) => {
|
||||
warn!("dropping malformed relay message frame from harness: {err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => continue,
|
||||
Some(Ok(Message::Text(_))) => {
|
||||
warn!("dropping non-binary relay message frame from harness");
|
||||
continue;
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!("multiplexed environment websocket read failed: {err}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let kind = match frame.validate() {
|
||||
Ok(kind) => kind,
|
||||
Err(err) => {
|
||||
warn!("dropping invalid relay message frame: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match kind {
|
||||
RelayFrameBodyKind::Data => {
|
||||
let stream_id = frame.stream_id.clone();
|
||||
let message = match frame.into_jsonrpc_message() {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
warn!("dropping malformed relay data message frame: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let stream = streams.entry(stream_id.clone()).or_insert_with(|| {
|
||||
spawn_virtual_stream(
|
||||
stream_id.clone(),
|
||||
processor.clone(),
|
||||
physical_outgoing_tx.clone(),
|
||||
)
|
||||
});
|
||||
if stream
|
||||
.incoming_tx
|
||||
.send(JsonRpcConnectionEvent::Message(message))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
streams.remove(&stream_id);
|
||||
}
|
||||
}
|
||||
RelayFrameBodyKind::Reset => {
|
||||
if let Some(stream) = streams.remove(&frame.stream_id) {
|
||||
stream.disconnect(frame.into_reset_reason()).await;
|
||||
}
|
||||
}
|
||||
RelayFrameBodyKind::Ack
|
||||
| RelayFrameBodyKind::Resume
|
||||
| RelayFrameBodyKind::Heartbeat => {}
|
||||
}
|
||||
}
|
||||
|
||||
for (_stream_id, stream) in streams {
|
||||
stream.disconnect(/*reason*/ None).await;
|
||||
}
|
||||
drop(physical_outgoing_tx);
|
||||
}
|
||||
|
||||
struct VirtualStream {
|
||||
incoming_tx: mpsc::Sender<JsonRpcConnectionEvent>,
|
||||
disconnected_tx: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
impl VirtualStream {
|
||||
async fn disconnect(self, reason: Option<String>) {
|
||||
let _ = self.disconnected_tx.send(true);
|
||||
let _ = self
|
||||
.incoming_tx
|
||||
.send(JsonRpcConnectionEvent::Disconnected { reason })
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_virtual_stream(
|
||||
stream_id: String,
|
||||
processor: ConnectionProcessor,
|
||||
physical_outgoing_tx: mpsc::Sender<Vec<u8>>,
|
||||
) -> VirtualStream {
|
||||
let (json_outgoing_tx, mut json_outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (disconnected_tx, disconnected_rx) = watch::channel(false);
|
||||
|
||||
let writer_stream_id = stream_id;
|
||||
let writer_task = tokio::spawn(async move {
|
||||
let mut next_seq = 0u32;
|
||||
while let Some(message) = json_outgoing_rx.recv().await {
|
||||
let payload = match jsonrpc_payload(&message) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
warn!("failed to serialize virtual stream JSON-RPC payload: {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
let frame = RelayMessageFrame::data(writer_stream_id.clone(), next_seq, payload);
|
||||
next_seq = next_seq.wrapping_add(1);
|
||||
if physical_outgoing_tx
|
||||
.send(encode_relay_message_frame(&frame))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connection = JsonRpcConnection {
|
||||
outgoing_tx: json_outgoing_tx,
|
||||
incoming_rx,
|
||||
disconnected_rx,
|
||||
task_handles: vec![writer_task],
|
||||
transport: JsonRpcTransport::Plain,
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
processor.run_connection(connection).await;
|
||||
});
|
||||
|
||||
VirtualStream {
|
||||
incoming_tx,
|
||||
disconnected_tx,
|
||||
transport: JsonRpcTransport::External,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,6 +383,7 @@ mod tests {
|
||||
use futures::task::AtomicWaker;
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
mod generated;
|
||||
|
||||
pub(crate) use generated::RelayData;
|
||||
pub(crate) use generated::RelayHandshake;
|
||||
pub(crate) use generated::RelayMessageFrame;
|
||||
pub(crate) use generated::RelayReset;
|
||||
pub(crate) use generated::RelayResume;
|
||||
pub(crate) use generated::relay_message_frame;
|
||||
|
||||
@@ -3,18 +3,28 @@ use std::time::Duration;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::time::sleep;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::connect_async_with_config;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::relay::run_multiplexed_environment;
|
||||
use crate::SecureChannelIdentity;
|
||||
use crate::SecureChannelPublicKey;
|
||||
use crate::secure_relay::HarnessKeyValidator;
|
||||
use crate::secure_relay::run_secure_multiplexed_environment;
|
||||
use crate::secure_relay::secure_relay_websocket_config;
|
||||
use crate::server::ConnectionProcessor;
|
||||
|
||||
const ERROR_BODY_PREVIEW_BYTES: usize = 4096;
|
||||
const ENVIRONMENT_REGISTRY_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const MAX_REMOTE_ENVIRONMENT_ID_LEN: usize = 256;
|
||||
const REMOTE_RENDEZVOUS_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EnvironmentRegistryClient {
|
||||
@@ -40,6 +50,7 @@ impl EnvironmentRegistryClient {
|
||||
auth_provider,
|
||||
http: reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(ENVIRONMENT_REGISTRY_REQUEST_TIMEOUT)
|
||||
.build()?,
|
||||
})
|
||||
}
|
||||
@@ -47,6 +58,7 @@ impl EnvironmentRegistryClient {
|
||||
async fn register_environment(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
executor_public_key: &SecureChannelPublicKey,
|
||||
) -> Result<EnvironmentRegistryRegistrationResponse, ExecServerError> {
|
||||
let response = self
|
||||
.http
|
||||
@@ -55,11 +67,41 @@ impl EnvironmentRegistryClient {
|
||||
&format!("/cloud/environment/{environment_id}/register"),
|
||||
))
|
||||
.headers(self.auth_provider.to_auth_headers())
|
||||
.json(&EnvironmentRegistryRegistrationRequest {
|
||||
security_profile: SECURE_RELAY_SECURITY_PROFILE,
|
||||
executor_public_key,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
self.parse_json_response(response).await
|
||||
}
|
||||
|
||||
async fn validate_harness_key(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
executor_registration_id: &str,
|
||||
harness_public_key: &SecureChannelPublicKey,
|
||||
harness_key_authorization: &str,
|
||||
) -> Result<(), ExecServerError> {
|
||||
let response = self
|
||||
.http
|
||||
.post(endpoint_url(
|
||||
&self.base_url,
|
||||
&format!("/cloud/environment/{environment_id}/validate-harness-key"),
|
||||
))
|
||||
.headers(self.auth_provider.to_auth_headers())
|
||||
.json(&EnvironmentRegistryHarnessKeyValidationRequest {
|
||||
executor_registration_id,
|
||||
harness_public_key,
|
||||
harness_key_authorization,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
self.parse_json_response::<EnvironmentRegistryHarnessKeyValidationResponse>(response)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn parse_json_response<R>(
|
||||
&self,
|
||||
response: reqwest::Response,
|
||||
@@ -81,10 +123,54 @@ impl EnvironmentRegistryClient {
|
||||
}
|
||||
}
|
||||
|
||||
const SECURE_RELAY_SECURITY_PROFILE: &str = "noise_hybrid_ik_v1";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EnvironmentRegistryRegistrationRequest<'a> {
|
||||
security_profile: &'static str,
|
||||
executor_public_key: &'a SecureChannelPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
struct EnvironmentRegistryRegistrationResponse {
|
||||
environment_id: String,
|
||||
url: String,
|
||||
security_profile: String,
|
||||
executor_registration_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EnvironmentRegistryHarnessKeyValidationRequest<'a> {
|
||||
executor_registration_id: &'a str,
|
||||
harness_public_key: &'a SecureChannelPublicKey,
|
||||
harness_key_authorization: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EnvironmentRegistryHarnessKeyValidationResponse {}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RegistryHarnessKeyValidator {
|
||||
client: EnvironmentRegistryClient,
|
||||
environment_id: String,
|
||||
executor_registration_id: String,
|
||||
}
|
||||
|
||||
impl HarnessKeyValidator for RegistryHarnessKeyValidator {
|
||||
async fn validate_harness_key(
|
||||
&self,
|
||||
harness_public_key: &SecureChannelPublicKey,
|
||||
authorization: &str,
|
||||
) -> Result<(), ExecServerError> {
|
||||
self.client
|
||||
.validate_harness_key(
|
||||
&self.environment_id,
|
||||
&self.executor_registration_id,
|
||||
harness_public_key,
|
||||
authorization,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for registering an exec-server for remote use.
|
||||
@@ -133,23 +219,60 @@ pub async fn run_remote_environment(
|
||||
let client =
|
||||
EnvironmentRegistryClient::new(config.base_url.clone(), config.auth_provider.clone())?;
|
||||
let processor = ConnectionProcessor::new(runtime_paths);
|
||||
let identity = SecureChannelIdentity::generate()?;
|
||||
let executor_public_key = identity.public_key();
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
|
||||
loop {
|
||||
let response = client.register_environment(&config.environment_id).await?;
|
||||
eprintln!(
|
||||
"codex exec-server remote environment registered with environment_id {}",
|
||||
response.environment_id
|
||||
let response = client
|
||||
.register_environment(&config.environment_id, &executor_public_key)
|
||||
.await?;
|
||||
if response.security_profile != SECURE_RELAY_SECURITY_PROFILE {
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"environment registry returned unsupported security profile `{}`",
|
||||
response.security_profile
|
||||
)));
|
||||
}
|
||||
if response.environment_id != config.environment_id {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"environment registry returned a different environment id".to_string(),
|
||||
));
|
||||
}
|
||||
let environment_id = &response.environment_id;
|
||||
info!(
|
||||
"codex exec-server remote environment registered with environment_id {environment_id}"
|
||||
);
|
||||
|
||||
match connect_async(response.url.as_str()).await {
|
||||
Ok((websocket, _)) => {
|
||||
match timeout(
|
||||
REMOTE_RENDEZVOUS_CONNECT_TIMEOUT,
|
||||
connect_async_with_config(
|
||||
response.url.as_str(),
|
||||
Some(secure_relay_websocket_config()),
|
||||
/*disable_nagle*/ false,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok((websocket, _))) => {
|
||||
backoff = Duration::from_secs(1);
|
||||
run_multiplexed_environment(websocket, processor.clone()).await;
|
||||
run_secure_multiplexed_environment(
|
||||
websocket,
|
||||
processor.clone(),
|
||||
response.environment_id,
|
||||
response.executor_registration_id.clone(),
|
||||
identity.clone(),
|
||||
RegistryHarnessKeyValidator {
|
||||
client: client.clone(),
|
||||
environment_id: config.environment_id.clone(),
|
||||
executor_registration_id: response.executor_registration_id,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
Ok(Err(err)) => {
|
||||
warn!("failed to connect remote exec-server websocket: {err}");
|
||||
}
|
||||
Err(_) => warn!("timed out connecting remote exec-server websocket"),
|
||||
}
|
||||
|
||||
sleep(backoff).await;
|
||||
@@ -158,12 +281,32 @@ pub async fn run_remote_environment(
|
||||
}
|
||||
|
||||
fn normalize_environment_id(environment_id: String) -> Result<String, ExecServerError> {
|
||||
let environment_id = environment_id.trim().to_string();
|
||||
if environment_id.is_empty() {
|
||||
return Err(ExecServerError::EnvironmentRegistryConfig(
|
||||
"environment id is required for remote exec-server registration".to_string(),
|
||||
));
|
||||
}
|
||||
if environment_id.trim() != environment_id {
|
||||
return Err(ExecServerError::EnvironmentRegistryConfig(
|
||||
"environment id must not contain surrounding whitespace".to_string(),
|
||||
));
|
||||
}
|
||||
if environment_id.len() > MAX_REMOTE_ENVIRONMENT_ID_LEN {
|
||||
return Err(ExecServerError::EnvironmentRegistryConfig(format!(
|
||||
"environment id cannot be longer than {MAX_REMOTE_ENVIRONMENT_ID_LEN} characters"
|
||||
)));
|
||||
}
|
||||
// The ID is interpolated into authenticated registry request paths below.
|
||||
// Keep it to one URL path component so a caller cannot use a delimiter to
|
||||
// redirect the exec-server's registration credential to another endpoint.
|
||||
if !environment_id
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
|
||||
{
|
||||
return Err(ExecServerError::EnvironmentRegistryConfig(
|
||||
"environment id must contain only ASCII letters, numbers, '-' or '_'".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(environment_id)
|
||||
}
|
||||
|
||||
@@ -242,125 +385,5 @@ fn preview_error_body(body: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_api::AuthProvider;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use pretty_assertions::assert_eq;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StaticRegistryAuthProvider;
|
||||
|
||||
impl AuthProvider for StaticRegistryAuthProvider {
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap) {
|
||||
let _ = headers.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
HeaderValue::from_static("Bearer registry-token"),
|
||||
);
|
||||
let _ = headers.insert(
|
||||
"ChatGPT-Account-ID",
|
||||
HeaderValue::from_static("workspace-123"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn static_registry_auth_provider() -> SharedAuthProvider {
|
||||
Arc::new(StaticRegistryAuthProvider)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_environment_posts_with_auth_provider_headers() {
|
||||
let server = MockServer::start().await;
|
||||
let config = RemoteEnvironmentConfig::new(
|
||||
server.uri(),
|
||||
"environment-requested".to_string(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect("config");
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/cloud/environment/environment-requested/register"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.and(header("chatgpt-account-id", "workspace-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"environment_id": "env-1",
|
||||
"url": "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc"
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = EnvironmentRegistryClient::new(server.uri(), static_registry_auth_provider())
|
||||
.expect("client");
|
||||
|
||||
let response = client
|
||||
.register_environment(&config.environment_id)
|
||||
.await
|
||||
.expect("register environment");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
EnvironmentRegistryRegistrationResponse {
|
||||
environment_id: "env-1".to_string(),
|
||||
url: "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_environment_does_not_follow_redirects_with_auth_headers() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/cloud/environment/environment-requested/register"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(302)
|
||||
.insert_header("location", format!("{}/redirect-target", server.uri())),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(path("/redirect-target"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(0)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = EnvironmentRegistryClient::new(server.uri(), static_registry_auth_provider())
|
||||
.expect("client");
|
||||
|
||||
let error = client
|
||||
.register_environment("environment-requested")
|
||||
.await
|
||||
.expect_err("redirect response should not be followed");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
ExecServerError::EnvironmentRegistryHttp {
|
||||
status: StatusCode::FOUND,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_output_redacts_auth_provider() {
|
||||
let config = RemoteEnvironmentConfig::new(
|
||||
"https://registry.example".to_string(),
|
||||
"env-1".to_string(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect("config");
|
||||
|
||||
let debug = format!("{config:?}");
|
||||
|
||||
assert!(debug.contains("<redacted>"));
|
||||
assert!(!debug.contains("workspace-123"));
|
||||
}
|
||||
}
|
||||
#[path = "remote_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
160
codex-rs/exec-server/src/remote_tests.rs
Normal file
160
codex-rs/exec-server/src/remote_tests.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_api::AuthProvider;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::StatusCode;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StaticRegistryAuthProvider;
|
||||
|
||||
impl AuthProvider for StaticRegistryAuthProvider {
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap) {
|
||||
let _ = headers.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
HeaderValue::from_static("Bearer registry-token"),
|
||||
);
|
||||
let _ = headers.insert(
|
||||
"ChatGPT-Account-ID",
|
||||
HeaderValue::from_static("workspace-123"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn static_registry_auth_provider() -> SharedAuthProvider {
|
||||
Arc::new(StaticRegistryAuthProvider)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_environment_posts_with_auth_provider_headers() {
|
||||
let server = MockServer::start().await;
|
||||
let config = RemoteEnvironmentConfig::new(
|
||||
server.uri(),
|
||||
"environment-requested".to_string(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect("config");
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/cloud/environment/environment-requested/register"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.and(header("chatgpt-account-id", "workspace-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"environment_id": "env-1",
|
||||
"url": "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc",
|
||||
"security_profile": SECURE_RELAY_SECURITY_PROFILE,
|
||||
"executor_registration_id": "registration-1",
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = EnvironmentRegistryClient::new(server.uri(), static_registry_auth_provider())
|
||||
.expect("client");
|
||||
let executor_public_key = SecureChannelIdentity::generate()
|
||||
.expect("identity")
|
||||
.public_key();
|
||||
|
||||
let response = client
|
||||
.register_environment(&config.environment_id, &executor_public_key)
|
||||
.await
|
||||
.expect("register environment");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
EnvironmentRegistryRegistrationResponse {
|
||||
environment_id: "env-1".to_string(),
|
||||
url: "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc".to_string(),
|
||||
security_profile: SECURE_RELAY_SECURITY_PROFILE.to_string(),
|
||||
executor_registration_id: "registration-1".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_environment_does_not_follow_redirects_with_auth_headers() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/cloud/environment/environment-requested/register"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(302)
|
||||
.insert_header("location", format!("{}/redirect-target", server.uri())),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(path("/redirect-target"))
|
||||
.and(header("authorization", "Bearer registry-token"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(0)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = EnvironmentRegistryClient::new(server.uri(), static_registry_auth_provider())
|
||||
.expect("client");
|
||||
let executor_public_key = SecureChannelIdentity::generate()
|
||||
.expect("identity")
|
||||
.public_key();
|
||||
|
||||
let error = client
|
||||
.register_environment("environment-requested", &executor_public_key)
|
||||
.await
|
||||
.expect_err("redirect response should not be followed");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
ExecServerError::EnvironmentRegistryHttp {
|
||||
status: StatusCode::FOUND,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_output_redacts_auth_provider() {
|
||||
let config = RemoteEnvironmentConfig::new(
|
||||
"https://registry.example".to_string(),
|
||||
"env-1".to_string(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect("config");
|
||||
|
||||
let debug = format!("{config:?}");
|
||||
|
||||
assert!(debug.contains("<redacted>"));
|
||||
assert!(!debug.contains("workspace-123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_environment_config_accepts_cloud_environment_id() {
|
||||
let environment_id = "ccarenv_b64_Y2Fhcy1zdGFnaW5nLWV4ZWN1dG9yLWVudmlyb25tZW50LTE".to_string();
|
||||
|
||||
let config = RemoteEnvironmentConfig::new(
|
||||
"https://registry.example".to_string(),
|
||||
environment_id.clone(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect("config");
|
||||
|
||||
assert_eq!(config.environment_id, environment_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_environment_config_rejects_registry_path_injection() {
|
||||
let error = RemoteEnvironmentConfig::new(
|
||||
"https://registry.example".to_string(),
|
||||
"ccarenv_b64_valid/../../status".to_string(),
|
||||
static_registry_auth_provider(),
|
||||
)
|
||||
.expect_err("path delimiter must not reach an authenticated registry request");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
ExecServerError::EnvironmentRegistryConfig(message) if message.contains("ASCII letters")
|
||||
));
|
||||
}
|
||||
374
codex-rs/exec-server/src/secure_channel.rs
Normal file
374
codex-rs/exec-server/src/secure_channel.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
//! Narrow, misuse-resistant wrapper around the Clatter primitives used by the
|
||||
//! remote exec-server relay.
|
||||
//!
|
||||
//! The harness is the Noise hybrid-IK initiator and pins the registry-provided
|
||||
//! exec-server static public key. The exec-server is the responder; after it
|
||||
//! reads the first IK message, Noise exposes the authenticated harness static
|
||||
//! public key for registry authorization. Application data is allowed only
|
||||
//! after both the cryptographic handshake and that external authorization pass.
|
||||
|
||||
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::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;
|
||||
|
||||
use crate::aws_lc_ml_kem::AwsLcMlKem768;
|
||||
use crate::aws_lc_ml_kem::PUBLIC_KEY_LEN as MLKEM768_PUBLIC_KEY_LEN;
|
||||
|
||||
pub const SECURE_CHANNEL_SUITE: &str = "Noise_hybridIK_X25519+MLKEM768_AESGCM_SHA256";
|
||||
|
||||
const X25519_PUBLIC_KEY_LEN: usize = 32;
|
||||
const MAX_TRANSPORT_RECORDS_PER_DIRECTION: u64 = u32::MAX as u64;
|
||||
const PROLOGUE_DOMAIN: &[u8] = b"codex-exec-server-relay-noise/v1";
|
||||
|
||||
type Handshake = HybridHandshake<X25519, AwsLcMlKem768, AwsLcMlKem768, AesGcm, Sha256>;
|
||||
type Transport = TransportState<AesGcm, Sha256>;
|
||||
type DhKeyPair = KeyPair<<X25519 as Dh>::PubKey, <X25519 as Dh>::PrivateKey>;
|
||||
type KemKeyPair = KeyPair<<AwsLcMlKem768 as Kem>::PubKey, <AwsLcMlKem768 as Kem>::SecretKey>;
|
||||
|
||||
/// Public key material for the exec-server secure relay Noise suite.
|
||||
///
|
||||
/// The suite field is part of the serialized contract. A key from a different
|
||||
/// suite must not be interpreted as compatible merely because one component has
|
||||
/// a familiar byte length.
|
||||
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SecureChannelPublicKey {
|
||||
suite: String,
|
||||
x25519_public_key: String,
|
||||
mlkem768_public_key: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecureChannelPublicKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecureChannelPublicKey")
|
||||
.field("suite", &self.suite)
|
||||
.field("x25519_public_key", &"<redacted>")
|
||||
.field("mlkem768_public_key", &"<redacted>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecureChannelPublicKey {
|
||||
/// Validate suite selection, encoding, and exact key lengths.
|
||||
pub fn validate(&self) -> Result<(), SecureChannelError> {
|
||||
let _ = self.decode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn from_keypairs(dh: &DhKeyPair, kem: &KemKeyPair) -> Self {
|
||||
Self {
|
||||
suite: SECURE_CHANNEL_SUITE.to_string(),
|
||||
x25519_public_key: STANDARD.encode(dh.public),
|
||||
mlkem768_public_key: STANDARD.encode(kem.public.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_raw(dh: &<X25519 as Dh>::PubKey, kem: &<AwsLcMlKem768 as Kem>::PubKey) -> Self {
|
||||
Self {
|
||||
suite: SECURE_CHANNEL_SUITE.to_string(),
|
||||
x25519_public_key: STANDARD.encode(dh),
|
||||
mlkem768_public_key: STANDARD.encode(kem.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
) -> Result<(<X25519 as Dh>::PubKey, <AwsLcMlKem768 as Kem>::PubKey), SecureChannelError> {
|
||||
if self.suite != SECURE_CHANNEL_SUITE {
|
||||
return Err(SecureChannelError::InvalidPublicKey(
|
||||
"unsupported secure channel suite",
|
||||
));
|
||||
}
|
||||
let dh = STANDARD
|
||||
.decode(&self.x25519_public_key)
|
||||
.map_err(|_| SecureChannelError::InvalidPublicKey("invalid X25519 public key"))?;
|
||||
let dh: [u8; X25519_PUBLIC_KEY_LEN] = dh.try_into().map_err(|_| {
|
||||
SecureChannelError::InvalidPublicKey("invalid X25519 public key length")
|
||||
})?;
|
||||
let kem = STANDARD
|
||||
.decode(&self.mlkem768_public_key)
|
||||
.map_err(|_| SecureChannelError::InvalidPublicKey("invalid ML-KEM-768 public key"))?;
|
||||
if kem.len() != MLKEM768_PUBLIC_KEY_LEN {
|
||||
return Err(SecureChannelError::InvalidPublicKey(
|
||||
"invalid ML-KEM-768 public key length",
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
dh,
|
||||
<AwsLcMlKem768 as Kem>::PubKey::from_slice(kem.as_slice()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Endpoint-local static identity for the exec-server secure relay Noise suite.
|
||||
///
|
||||
/// Private components never cross the process boundary. Cloning is used only to
|
||||
/// construct Clatter handshake state for reconnects within the same process.
|
||||
#[derive(Clone)]
|
||||
pub struct SecureChannelIdentity {
|
||||
dh: DhKeyPair,
|
||||
kem: KemKeyPair,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SecureChannelIdentity {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SecureChannelIdentity")
|
||||
.field("public_key", &self.public_key())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecureChannelIdentity {
|
||||
/// Generate independent classical and post-quantum static keypairs.
|
||||
pub fn generate() -> Result<Self, SecureChannelError> {
|
||||
let dh = X25519::genkey()
|
||||
.map_err(|error| SecureChannelError::KeyGeneration(error.to_string()))?;
|
||||
let kem = AwsLcMlKem768::genkey()
|
||||
.map_err(|error| SecureChannelError::KeyGeneration(error.to_string()))?;
|
||||
Ok(Self { dh, kem })
|
||||
}
|
||||
|
||||
/// Return the distributable public half of this endpoint identity.
|
||||
pub fn public_key(&self) -> SecureChannelPublicKey {
|
||||
SecureChannelPublicKey::from_keypairs(&self.dh, &self.kem)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InitiatorHandshake {
|
||||
handshake: Handshake,
|
||||
}
|
||||
|
||||
impl InitiatorHandshake {
|
||||
/// Start hybrid IK while pinning the expected responder static key.
|
||||
///
|
||||
/// `payload` is encrypted by the first IK message. The relay uses it for
|
||||
/// the short-lived registry authorization naming this harness public key.
|
||||
pub(crate) fn start(
|
||||
identity: &SecureChannelIdentity,
|
||||
responder_public_key: &SecureChannelPublicKey,
|
||||
prologue: &[u8],
|
||||
payload: &[u8],
|
||||
) -> Result<(Self, Vec<u8>), SecureChannelError> {
|
||||
let (responder_dh, responder_kem) = responder_public_key.decode()?;
|
||||
|
||||
// IK authenticates both static identities. Supplying both responder
|
||||
// components here is what makes a misrouted or impersonating
|
||||
// exec-server fail before any JSON-RPC plaintext is released.
|
||||
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 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 responder message and enter transport mode.
|
||||
///
|
||||
/// The responder message carries no application payload in v1. Rejecting
|
||||
/// one keeps future protocol additions from becoming an implicit channel.
|
||||
pub(crate) fn finish(mut self, response: &[u8]) -> Result<SecureTransport, SecureChannelError> {
|
||||
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(SecureChannelError::InvalidMessage(
|
||||
"handshake response payload must be empty",
|
||||
));
|
||||
}
|
||||
Ok(SecureTransport {
|
||||
transport: self.handshake.finalize()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PendingResponderHandshake {
|
||||
handshake: Handshake,
|
||||
initiator_public_key: SecureChannelPublicKey,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PendingResponderHandshake {
|
||||
/// Authenticate and parse the first IK message without completing it.
|
||||
///
|
||||
/// This split is intentional: callers must authorize `initiator_public_key`
|
||||
/// with the registry before calling [`Self::complete`].
|
||||
pub(crate) fn read_request(
|
||||
identity: &SecureChannelIdentity,
|
||||
prologue: &[u8],
|
||||
request: &[u8],
|
||||
) -> Result<Self, SecureChannelError> {
|
||||
ensure_noise_frame_len(request.len(), "handshake request is too large")?;
|
||||
let params = HybridHandshakeParams::new(noise_hybrid_ik(), false)
|
||||
.with_prologue(prologue)
|
||||
.with_s(identity.dh.clone())
|
||||
.with_s_kem(identity.kem.clone());
|
||||
let mut handshake = Handshake::new(params)?;
|
||||
let mut payload = [0u8; MAX_MESSAGE_LEN];
|
||||
let payload_len = handshake.read_message(request, &mut payload)?;
|
||||
// Clatter exposes the initiator static key only after the first IK
|
||||
// message authenticates and decrypts successfully.
|
||||
let remote = handshake
|
||||
.get_remote_static()
|
||||
.ok_or(SecureChannelError::InvalidMessage(
|
||||
"handshake request is missing initiator static key",
|
||||
))?;
|
||||
Ok(Self {
|
||||
handshake,
|
||||
initiator_public_key: SecureChannelPublicKey::from_raw(remote.dh(), remote.kem()),
|
||||
payload: payload[..payload_len].to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn initiator_public_key(&self) -> &SecureChannelPublicKey {
|
||||
&self.initiator_public_key
|
||||
}
|
||||
|
||||
pub(crate) fn payload(&self) -> &[u8] {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
/// Finish the responder handshake after external harness authorization.
|
||||
pub(crate) fn complete(mut self) -> Result<(SecureTransport, Vec<u8>), SecureChannelError> {
|
||||
let mut response = [0u8; MAX_MESSAGE_LEN];
|
||||
let response_len = self.handshake.write_message(&[], &mut response)?;
|
||||
Ok((
|
||||
SecureTransport {
|
||||
transport: self.handshake.finalize()?,
|
||||
},
|
||||
response[..response_len].to_vec(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SecureTransport {
|
||||
transport: Transport,
|
||||
}
|
||||
|
||||
impl SecureTransport {
|
||||
/// Encrypt exactly one ordered transport record.
|
||||
///
|
||||
/// The caller owns relay sequence assignment and must never encrypt the
|
||||
/// same logical record twice under different transport nonces.
|
||||
pub(crate) fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, SecureChannelError> {
|
||||
if self.transport.sending_nonce() >= MAX_TRANSPORT_RECORDS_PER_DIRECTION {
|
||||
return Err(SecureChannelError::InvalidState(
|
||||
"transport record nonce exhausted",
|
||||
));
|
||||
}
|
||||
let frame_len = plaintext.len().checked_add(AesGcm::tag_len()).ok_or(
|
||||
SecureChannelError::InvalidMessage("transport plaintext is too large"),
|
||||
)?;
|
||||
ensure_noise_frame_len(frame_len, "transport plaintext is too large")?;
|
||||
Ok(self.transport.send_vec(plaintext)?)
|
||||
}
|
||||
|
||||
/// Decrypt exactly the next ordered transport record.
|
||||
///
|
||||
/// Clatter advances the receiving nonce during this call, so callers must
|
||||
/// reorder and deduplicate relay frames before invoking it.
|
||||
pub(crate) fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, SecureChannelError> {
|
||||
if self.transport.receiving_nonce() >= MAX_TRANSPORT_RECORDS_PER_DIRECTION {
|
||||
return Err(SecureChannelError::InvalidState(
|
||||
"transport record nonce exhausted",
|
||||
));
|
||||
}
|
||||
if ciphertext.len() < AesGcm::tag_len() {
|
||||
return Err(SecureChannelError::InvalidMessage(
|
||||
"transport ciphertext is too short",
|
||||
));
|
||||
}
|
||||
ensure_noise_frame_len(ciphertext.len(), "transport ciphertext is too large")?;
|
||||
Ok(self.transport.receive_vec(ciphertext)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the transcript prologue that binds cryptographic identity to routing.
|
||||
///
|
||||
/// A handshake captured from one environment, exec-server registration, or
|
||||
/// relay stream cannot be replayed into another because every participant must
|
||||
/// construct the same prologue before the first Noise message is processed.
|
||||
pub(crate) fn secure_channel_prologue(
|
||||
environment_id: &str,
|
||||
executor_registration_id: &str,
|
||||
stream_id: &str,
|
||||
) -> Result<Vec<u8>, SecureChannelError> {
|
||||
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())?;
|
||||
Ok(prologue)
|
||||
}
|
||||
|
||||
fn append_prologue_part(prologue: &mut Vec<u8>, part: &[u8]) -> Result<(), SecureChannelError> {
|
||||
// Length prefixes make component boundaries unambiguous. Raw concatenation
|
||||
// would allow different identifier tuples to produce the same prologue.
|
||||
let len = u32::try_from(part.len()).map_err(|_| {
|
||||
SecureChannelError::InvalidMessage("secure channel prologue part is too large")
|
||||
})?;
|
||||
prologue.extend_from_slice(&len.to_be_bytes());
|
||||
prologue.extend_from_slice(part);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_noise_frame_len(
|
||||
frame_len: usize,
|
||||
message: &'static str,
|
||||
) -> Result<(), SecureChannelError> {
|
||||
if frame_len > MAX_MESSAGE_LEN {
|
||||
return Err(SecureChannelError::InvalidMessage(message));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SecureChannelError {
|
||||
#[error("secure channel key generation failed: {0}")]
|
||||
KeyGeneration(String),
|
||||
#[error("invalid secure channel public key: {0}")]
|
||||
InvalidPublicKey(&'static str),
|
||||
#[error("invalid secure channel state: {0}")]
|
||||
InvalidState(&'static str),
|
||||
#[error("invalid secure channel message: {0}")]
|
||||
InvalidMessage(&'static str),
|
||||
#[error("secure channel handshake failed: {0}")]
|
||||
Handshake(String),
|
||||
#[error("secure channel transport failed: {0}")]
|
||||
Transport(String),
|
||||
}
|
||||
|
||||
impl From<clatter::error::HandshakeError> for SecureChannelError {
|
||||
fn from(error: clatter::error::HandshakeError) -> Self {
|
||||
Self::Handshake(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<clatter::error::TransportError> for SecureChannelError {
|
||||
fn from(error: clatter::error::TransportError) -> Self {
|
||||
Self::Transport(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "secure_channel_tests.rs"]
|
||||
mod tests;
|
||||
252
codex-rs/exec-server/src/secure_channel_tests.rs
Normal file
252
codex-rs/exec-server/src/secure_channel_tests.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::InitiatorHandshake;
|
||||
use super::MAX_TRANSPORT_RECORDS_PER_DIRECTION;
|
||||
use super::PendingResponderHandshake;
|
||||
use super::SECURE_CHANNEL_SUITE;
|
||||
use super::SecureChannelError;
|
||||
use super::SecureChannelIdentity;
|
||||
use super::SecureChannelPublicKey;
|
||||
use super::secure_channel_prologue;
|
||||
|
||||
#[test]
|
||||
fn hybrid_ik_roundtrip_authenticates_both_endpoints() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let responder = SecureChannelIdentity::generate().expect("generate responder identity");
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
let authorization = b"harness-key-authorization";
|
||||
|
||||
let (initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&responder.public_key(),
|
||||
&prologue,
|
||||
authorization,
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
let responder_handshake =
|
||||
PendingResponderHandshake::read_request(&responder, &prologue, &request)
|
||||
.expect("read responder handshake");
|
||||
|
||||
assert_eq!(
|
||||
responder_handshake.initiator_public_key(),
|
||||
&initiator.public_key()
|
||||
);
|
||||
assert_eq!(responder_handshake.payload(), authorization);
|
||||
|
||||
let (mut responder_transport, response) = responder_handshake
|
||||
.complete()
|
||||
.expect("complete responder handshake");
|
||||
let mut initiator_transport = initiator_handshake
|
||||
.finish(&response)
|
||||
.expect("complete initiator handshake");
|
||||
|
||||
let request_ciphertext = initiator_transport
|
||||
.encrypt(b"request")
|
||||
.expect("encrypt request");
|
||||
assert_ne!(request_ciphertext, b"request");
|
||||
assert_eq!(
|
||||
responder_transport
|
||||
.decrypt(&request_ciphertext)
|
||||
.expect("decrypt request"),
|
||||
b"request"
|
||||
);
|
||||
|
||||
let response_ciphertext = responder_transport
|
||||
.encrypt(b"response")
|
||||
.expect("encrypt response");
|
||||
assert_ne!(response_ciphertext, b"response");
|
||||
assert_eq!(
|
||||
initiator_transport
|
||||
.decrypt(&response_ciphertext)
|
||||
.expect("decrypt response"),
|
||||
b"response"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initiator_rejects_wrong_responder_key() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let expected_responder = SecureChannelIdentity::generate().expect("generate expected identity");
|
||||
let actual_responder = SecureChannelIdentity::generate().expect("generate actual identity");
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
|
||||
let (_initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&expected_responder.public_key(),
|
||||
&prologue,
|
||||
b"authorization",
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
|
||||
assert!(
|
||||
PendingResponderHandshake::read_request(&actual_responder, &prologue, &request).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responder_rejects_mismatched_prologue() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let responder = SecureChannelIdentity::generate().expect("generate responder identity");
|
||||
let initiator_prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
let responder_prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-2").expect("build prologue");
|
||||
let (_initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&responder.public_key(),
|
||||
&initiator_prologue,
|
||||
b"authorization",
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
|
||||
assert!(
|
||||
PendingResponderHandshake::read_request(&responder, &responder_prologue, &request).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prologue_encoding_is_stable_and_unambiguous() {
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
|
||||
assert_eq!(
|
||||
prologue,
|
||||
b"\x00\x00\x00\x20codex-exec-server-relay-noise/v1\
|
||||
\x00\x00\x00\x05env-1\
|
||||
\x00\x00\x00\x0eregistration-1\
|
||||
\x00\x00\x00\x08stream-1"
|
||||
.to_vec()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_rejects_tampered_ciphertext() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let responder = SecureChannelIdentity::generate().expect("generate responder identity");
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
let (initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&responder.public_key(),
|
||||
&prologue,
|
||||
b"authorization",
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
let responder_handshake =
|
||||
PendingResponderHandshake::read_request(&responder, &prologue, &request)
|
||||
.expect("read responder handshake");
|
||||
let (mut responder_transport, response) = responder_handshake
|
||||
.complete()
|
||||
.expect("complete responder handshake");
|
||||
let mut initiator_transport = initiator_handshake
|
||||
.finish(&response)
|
||||
.expect("complete initiator handshake");
|
||||
let mut ciphertext = initiator_transport
|
||||
.encrypt(b"request")
|
||||
.expect("encrypt request");
|
||||
ciphertext[0] ^= 1;
|
||||
|
||||
assert!(responder_transport.decrypt(&ciphertext).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_rejects_exhausted_receiving_nonce_before_decryption() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let responder = SecureChannelIdentity::generate().expect("generate responder identity");
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
let (initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&responder.public_key(),
|
||||
&prologue,
|
||||
b"authorization",
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
let responder_handshake =
|
||||
PendingResponderHandshake::read_request(&responder, &prologue, &request)
|
||||
.expect("read responder handshake");
|
||||
let (mut responder_transport, response) = responder_handshake
|
||||
.complete()
|
||||
.expect("complete responder handshake");
|
||||
let mut initiator_transport = initiator_handshake
|
||||
.finish(&response)
|
||||
.expect("complete initiator handshake");
|
||||
let ciphertext = initiator_transport
|
||||
.encrypt(b"request")
|
||||
.expect("encrypt request");
|
||||
responder_transport
|
||||
.transport
|
||||
.set_receiving_nonce(MAX_TRANSPORT_RECORDS_PER_DIRECTION);
|
||||
|
||||
assert!(matches!(
|
||||
responder_transport.decrypt(&ciphertext),
|
||||
Err(SecureChannelError::InvalidState(
|
||||
"transport record nonce exhausted"
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_rejects_replayed_ciphertext() {
|
||||
let initiator = SecureChannelIdentity::generate().expect("generate initiator identity");
|
||||
let responder = SecureChannelIdentity::generate().expect("generate responder identity");
|
||||
let prologue =
|
||||
secure_channel_prologue("env-1", "registration-1", "stream-1").expect("build prologue");
|
||||
let (initiator_handshake, request) = InitiatorHandshake::start(
|
||||
&initiator,
|
||||
&responder.public_key(),
|
||||
&prologue,
|
||||
b"authorization",
|
||||
)
|
||||
.expect("start initiator handshake");
|
||||
let responder_handshake =
|
||||
PendingResponderHandshake::read_request(&responder, &prologue, &request)
|
||||
.expect("read responder handshake");
|
||||
let (mut responder_transport, response) = responder_handshake
|
||||
.complete()
|
||||
.expect("complete responder handshake");
|
||||
let mut initiator_transport = initiator_handshake
|
||||
.finish(&response)
|
||||
.expect("complete initiator handshake");
|
||||
let ciphertext = initiator_transport
|
||||
.encrypt(b"request")
|
||||
.expect("encrypt request");
|
||||
|
||||
assert_eq!(
|
||||
responder_transport
|
||||
.decrypt(&ciphertext)
|
||||
.expect("decrypt request"),
|
||||
b"request"
|
||||
);
|
||||
assert!(matches!(
|
||||
responder_transport.decrypt(&ciphertext),
|
||||
Err(SecureChannelError::Transport(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_validation_rejects_unknown_suite() {
|
||||
let key = SecureChannelIdentity::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: SecureChannelPublicKey =
|
||||
serde_json::from_value(serde_json::Value::Object(object)).expect("deserialize key");
|
||||
|
||||
assert!(key.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_serializes_with_expected_suite() {
|
||||
let key = SecureChannelIdentity::generate()
|
||||
.expect("generate identity")
|
||||
.public_key();
|
||||
|
||||
let json = serde_json::to_value(key).expect("serialize key");
|
||||
|
||||
assert_eq!(json["suite"], SECURE_CHANNEL_SUITE);
|
||||
}
|
||||
487
codex-rs/exec-server/src/secure_relay/environment.rs
Normal file
487
codex-rs/exec-server/src/secure_relay/environment.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerError;
|
||||
use crate::connection::CHANNEL_CAPACITY;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::connection::JsonRpcConnectionEvent;
|
||||
use crate::connection::JsonRpcTransport;
|
||||
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;
|
||||
use crate::secure_channel::PendingResponderHandshake;
|
||||
use crate::secure_channel::SecureChannelIdentity;
|
||||
use crate::secure_channel::SecureChannelPublicKey;
|
||||
use crate::secure_channel::SecureTransport;
|
||||
use crate::secure_channel::secure_channel_prologue;
|
||||
use crate::secure_relay::message_framing::JsonRpcMessageDecoder;
|
||||
use crate::secure_relay::message_framing::SECURE_RECORD_PLAINTEXT_LEN;
|
||||
use crate::secure_relay::message_framing::frame_jsonrpc_message;
|
||||
use crate::secure_relay::ordered_ciphertext::OrderedCiphertextFrames;
|
||||
use crate::secure_relay::take_next_sequence;
|
||||
use crate::server::ConnectionProcessor;
|
||||
|
||||
const SECURE_RELAY_RESET_REASON: &str = "secure_relay_protocol_error";
|
||||
const MAX_ACTIVE_SECURE_RELAY_STREAMS: usize = 128;
|
||||
const MAX_PENDING_HANDSHAKE_VALIDATIONS: usize = 32;
|
||||
const HARNESS_KEY_VALIDATION_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Validates that a Noise-authenticated harness public key is authorized.
|
||||
///
|
||||
/// Implementations must consult an authority independent of rendezvous. The
|
||||
/// exec-server invokes this after parsing the first IK message and before
|
||||
/// completing the responder handshake.
|
||||
pub(crate) trait HarnessKeyValidator: Send + Sync {
|
||||
fn validate_harness_key(
|
||||
&self,
|
||||
harness_public_key: &SecureChannelPublicKey,
|
||||
authorization: &str,
|
||||
) -> impl std::future::Future<Output = Result<(), ExecServerError>> + Send;
|
||||
}
|
||||
|
||||
/// Serve many authenticated virtual JSON-RPC streams over one executor websocket.
|
||||
///
|
||||
/// Each stream has an independent Noise handshake and transport state. The
|
||||
/// outer websocket and rendezvous route are treated as untrusted delivery:
|
||||
/// malformed, unauthorized, or cryptographically invalid streams fail closed
|
||||
/// without creating a `JsonRpcConnection`.
|
||||
pub(crate) async fn run_secure_multiplexed_environment<S, V>(
|
||||
stream: WebSocketStream<S>,
|
||||
processor: ConnectionProcessor,
|
||||
environment_id: String,
|
||||
executor_registration_id: String,
|
||||
identity: SecureChannelIdentity,
|
||||
validator: V,
|
||||
) where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
V: HarnessKeyValidator + Clone + 'static,
|
||||
{
|
||||
let mut websocket = stream;
|
||||
let (physical_outgoing_tx, mut physical_outgoing_rx) =
|
||||
mpsc::channel::<Vec<u8>>(CHANNEL_CAPACITY);
|
||||
let mut streams: HashMap<String, SecureVirtualStream> = HashMap::new();
|
||||
let mut pending_handshakes: HashMap<String, PendingHandshake> = HashMap::new();
|
||||
let mut validation_tasks: JoinSet<HarnessKeyValidationResult> = JoinSet::new();
|
||||
let mut next_validation_id = 0u64;
|
||||
|
||||
loop {
|
||||
// Keep registry validation out of the main relay loop. A slow or
|
||||
// malicious authorization request must not block existing streams or
|
||||
// prevent other handshakes from being received and bounded.
|
||||
let frame = tokio::select! {
|
||||
maybe_encoded = physical_outgoing_rx.recv() => {
|
||||
let Some(encoded) = maybe_encoded else {
|
||||
break;
|
||||
};
|
||||
if websocket.send(Message::Binary(encoded.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
validation_result = validation_tasks.join_next(), if !validation_tasks.is_empty() => {
|
||||
match validation_result {
|
||||
Some(Ok(validation_result)) => {
|
||||
// Stream IDs may be reset and reused while validation
|
||||
// is in flight. The monotonic validation ID ensures a
|
||||
// stale task can never complete a newer handshake.
|
||||
let is_current = pending_handshakes
|
||||
.get(&validation_result.stream_id)
|
||||
.is_some_and(|pending| {
|
||||
pending.validation_id == validation_result.validation_id
|
||||
});
|
||||
if !is_current {
|
||||
continue;
|
||||
}
|
||||
let Some(pending) =
|
||||
pending_handshakes.remove(&validation_result.stream_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = validation_result.result {
|
||||
warn!("secure relay harness key validation failed: {error}");
|
||||
send_reset(&physical_outgoing_tx, validation_result.stream_id).await;
|
||||
continue;
|
||||
}
|
||||
if streams.len() >= MAX_ACTIVE_SECURE_RELAY_STREAMS {
|
||||
warn!("secure relay has too many active streams");
|
||||
send_reset(&physical_outgoing_tx, validation_result.stream_id).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is the only point where the responder completes
|
||||
// IK and exposes a JSON-RPC stream: Noise authenticated
|
||||
// the harness key and the registry authorized it.
|
||||
let (transport, response) = match pending.handshake.complete() {
|
||||
Ok(completed) => completed,
|
||||
Err(error) => {
|
||||
warn!("failed to complete secure relay handshake: {error}");
|
||||
send_reset(&physical_outgoing_tx, validation_result.stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let response = RelayMessageFrame::handshake(
|
||||
validation_result.stream_id.clone(),
|
||||
response,
|
||||
);
|
||||
if physical_outgoing_tx
|
||||
.send(encode_relay_message_frame(&response))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
streams.insert(
|
||||
validation_result.stream_id.clone(),
|
||||
spawn_secure_virtual_stream(
|
||||
validation_result.stream_id,
|
||||
processor.clone(),
|
||||
physical_outgoing_tx.clone(),
|
||||
transport,
|
||||
),
|
||||
);
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
warn!("secure relay harness key validation task failed: {error}");
|
||||
let stream_ids = pending_handshakes.keys().cloned().collect::<Vec<_>>();
|
||||
pending_handshakes.clear();
|
||||
for stream_id in stream_ids {
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
incoming_message = websocket.next() => match incoming_message {
|
||||
Some(Ok(Message::Binary(payload))) => match decode_relay_message_frame(payload.as_ref()) {
|
||||
Ok(frame) => frame,
|
||||
Err(error) => {
|
||||
warn!("dropping malformed secure relay frame from harness: {error}");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => continue,
|
||||
Some(Ok(Message::Text(_))) => {
|
||||
warn!("dropping non-binary secure relay frame from harness");
|
||||
continue;
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
debug!("secure multiplexed environment websocket read failed: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let kind = match frame.validate() {
|
||||
Ok(kind) => kind,
|
||||
Err(error) => {
|
||||
warn!("dropping invalid secure relay frame: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let stream_id = frame.stream_id.clone();
|
||||
match kind {
|
||||
RelayFrameBodyKind::Handshake => {
|
||||
// Bound all pre-authentication state before doing expensive
|
||||
// hybrid cryptography or starting an external validation.
|
||||
if streams.contains_key(&stream_id) || pending_handshakes.contains_key(&stream_id) {
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
if streams.len() >= MAX_ACTIVE_SECURE_RELAY_STREAMS {
|
||||
warn!("secure relay has too many active streams");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
if validation_tasks.len() >= MAX_PENDING_HANDSHAKE_VALIDATIONS {
|
||||
warn!("secure relay has too many pending harness key validations");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
let prologue = match secure_channel_prologue(
|
||||
&environment_id,
|
||||
&executor_registration_id,
|
||||
&stream_id,
|
||||
) {
|
||||
Ok(prologue) => prologue,
|
||||
Err(error) => {
|
||||
warn!("failed to build secure relay prologue: {error}");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let request = match frame.into_handshake_payload() {
|
||||
Ok(request) => request,
|
||||
Err(error) => {
|
||||
warn!("failed to read secure relay handshake frame: {error}");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let pending =
|
||||
match PendingResponderHandshake::read_request(&identity, &prologue, &request) {
|
||||
Ok(pending) => pending,
|
||||
Err(error) => {
|
||||
warn!("failed to read secure relay handshake request: {error}");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// The authorization is encrypted inside the first IK message.
|
||||
// It is meaningful only alongside the initiator static key
|
||||
// that Clatter authenticated from that same message.
|
||||
let authorization = match std::str::from_utf8(pending.payload()) {
|
||||
Ok(authorization) => authorization.to_string(),
|
||||
Err(_) => {
|
||||
warn!("secure relay handshake authorization is not UTF-8");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let harness_public_key = pending.initiator_public_key().clone();
|
||||
let validation_id = next_validation_id;
|
||||
let Some(next_id) = next_validation_id.checked_add(1) else {
|
||||
warn!("secure relay harness key validation id exhausted");
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
};
|
||||
next_validation_id = next_id;
|
||||
pending_handshakes.insert(
|
||||
stream_id.clone(),
|
||||
PendingHandshake {
|
||||
validation_id,
|
||||
handshake: pending,
|
||||
},
|
||||
);
|
||||
let validator = validator.clone();
|
||||
|
||||
// Validation is time-bounded and concurrency-bounded above.
|
||||
// Failure leaves no transport state and returns a generic
|
||||
// protocol reset to avoid exposing authorization details.
|
||||
validation_tasks.spawn(async move {
|
||||
let result = match timeout(
|
||||
HARNESS_KEY_VALIDATION_TIMEOUT,
|
||||
validator.validate_harness_key(&harness_public_key, &authorization),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ExecServerError::Protocol(
|
||||
"timed out validating secure relay harness key".to_string(),
|
||||
)),
|
||||
};
|
||||
HarnessKeyValidationResult {
|
||||
stream_id,
|
||||
validation_id,
|
||||
result,
|
||||
}
|
||||
});
|
||||
}
|
||||
RelayFrameBodyKind::Data => {
|
||||
// Data before handshake completion is always invalid. Removing
|
||||
// a pending handshake also ensures a peer cannot keep its
|
||||
// authorization task alive while sending application records.
|
||||
let Some(stream) = streams.get_mut(&stream_id) else {
|
||||
pending_handshakes.remove(&stream_id);
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
};
|
||||
let data = match frame.into_data() {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
warn!("dropping malformed secure relay data frame: {error}");
|
||||
streams.remove(&stream_id);
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(error) = stream.receive_data(data).await {
|
||||
warn!("failed to process secure relay payload: {error}");
|
||||
streams.remove(&stream_id);
|
||||
send_reset(&physical_outgoing_tx, stream_id).await;
|
||||
}
|
||||
}
|
||||
RelayFrameBodyKind::Reset => {
|
||||
pending_handshakes.remove(&stream_id);
|
||||
if let Some(stream) = streams.remove(&stream_id) {
|
||||
stream.disconnect(frame.into_reset_reason()).await;
|
||||
}
|
||||
}
|
||||
RelayFrameBodyKind::Ack
|
||||
| RelayFrameBodyKind::Resume
|
||||
| RelayFrameBodyKind::Heartbeat => {}
|
||||
}
|
||||
}
|
||||
|
||||
for (_stream_id, stream) in streams {
|
||||
stream.disconnect(/*reason*/ None).await;
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingHandshake {
|
||||
validation_id: u64,
|
||||
handshake: PendingResponderHandshake,
|
||||
}
|
||||
|
||||
struct HarnessKeyValidationResult {
|
||||
stream_id: String,
|
||||
validation_id: u64,
|
||||
result: Result<(), ExecServerError>,
|
||||
}
|
||||
|
||||
struct SecureVirtualStream {
|
||||
incoming_tx: mpsc::Sender<JsonRpcConnectionEvent>,
|
||||
disconnected_tx: watch::Sender<bool>,
|
||||
transport: Arc<Mutex<SecureTransport>>,
|
||||
inbound_ciphertexts: OrderedCiphertextFrames,
|
||||
inbound_decoder: JsonRpcMessageDecoder,
|
||||
}
|
||||
|
||||
impl SecureVirtualStream {
|
||||
async fn disconnect(self, reason: Option<String>) {
|
||||
let _ = self.disconnected_tx.send(true);
|
||||
let _ = self
|
||||
.incoming_tx
|
||||
.send(JsonRpcConnectionEvent::Disconnected { reason })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn receive_data(&mut self, data: RelayData) -> Result<(), ExecServerError> {
|
||||
// Relay sequence ordering is enforced before taking the transport lock
|
||||
// and decrypting. Each virtual stream owns one ordered Noise nonce
|
||||
// space shared by its reader and writer transport halves.
|
||||
for ciphertext in self.inbound_ciphertexts.push(data.seq, data.payload)? {
|
||||
let plaintext = {
|
||||
let mut transport = self
|
||||
.transport
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
transport.decrypt(&ciphertext)?
|
||||
};
|
||||
for message in self.inbound_decoder.push(&plaintext)? {
|
||||
self.incoming_tx
|
||||
.send(JsonRpcConnectionEvent::Message(message))
|
||||
.await
|
||||
.map_err(|_| ExecServerError::Closed)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_secure_virtual_stream(
|
||||
stream_id: String,
|
||||
processor: ConnectionProcessor,
|
||||
physical_outgoing_tx: mpsc::Sender<Vec<u8>>,
|
||||
transport: SecureTransport,
|
||||
) -> SecureVirtualStream {
|
||||
let (json_outgoing_tx, mut json_outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let (disconnected_tx, disconnected_rx) = watch::channel(false);
|
||||
let transport = Arc::new(Mutex::new(transport));
|
||||
let writer_transport = Arc::clone(&transport);
|
||||
let writer_stream_id = stream_id;
|
||||
let writer_task = tokio::spawn(async move {
|
||||
let mut next_seq = 0u32;
|
||||
'writer: while let Some(message) = json_outgoing_rx.recv().await {
|
||||
// Frame first, then split into bounded Noise records. Each record
|
||||
// receives one checked relay sequence and is encrypted exactly
|
||||
// once, preserving the implicit Noise sending nonce.
|
||||
let framed = match frame_jsonrpc_message(&message) {
|
||||
Ok(framed) => framed,
|
||||
Err(error) => {
|
||||
warn!("failed to frame secure virtual stream JSON-RPC payload: {error}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
for plaintext_record in framed.chunks(SECURE_RECORD_PLAINTEXT_LEN) {
|
||||
let seq = match take_next_sequence(&mut next_seq) {
|
||||
Ok(seq) => seq,
|
||||
Err(error) => {
|
||||
warn!("secure virtual stream sequence exhausted: {error}");
|
||||
break 'writer;
|
||||
}
|
||||
};
|
||||
let ciphertext = {
|
||||
let mut transport = writer_transport
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
transport.encrypt(plaintext_record)
|
||||
};
|
||||
let ciphertext = match ciphertext {
|
||||
Ok(ciphertext) => ciphertext,
|
||||
Err(error) => {
|
||||
warn!("failed to encrypt secure virtual stream payload: {error}");
|
||||
break 'writer;
|
||||
}
|
||||
};
|
||||
let frame = RelayMessageFrame::data(writer_stream_id.clone(), seq, ciphertext);
|
||||
if physical_outgoing_tx
|
||||
.send(encode_relay_message_frame(&frame))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break 'writer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the harness to discard this virtual stream whenever its writer
|
||||
// exits, including processor shutdown or a cryptographic/send failure.
|
||||
// Otherwise the peer could wait indefinitely on a dead stream.
|
||||
let reset =
|
||||
RelayMessageFrame::reset(writer_stream_id, SECURE_RELAY_RESET_REASON.to_string());
|
||||
let _ = physical_outgoing_tx
|
||||
.send(encode_relay_message_frame(&reset))
|
||||
.await;
|
||||
});
|
||||
|
||||
let connection = JsonRpcConnection {
|
||||
outgoing_tx: json_outgoing_tx,
|
||||
incoming_rx,
|
||||
disconnected_rx,
|
||||
task_handles: vec![writer_task],
|
||||
transport: JsonRpcTransport::External,
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
processor.run_connection(connection).await;
|
||||
});
|
||||
|
||||
SecureVirtualStream {
|
||||
incoming_tx,
|
||||
disconnected_tx,
|
||||
transport,
|
||||
inbound_ciphertexts: OrderedCiphertextFrames::default(),
|
||||
inbound_decoder: JsonRpcMessageDecoder::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_reset(physical_outgoing_tx: &mpsc::Sender<Vec<u8>>, stream_id: String) {
|
||||
let reset = RelayMessageFrame::reset(stream_id, SECURE_RELAY_RESET_REASON.to_string());
|
||||
let _ = physical_outgoing_tx
|
||||
.send(encode_relay_message_frame(&reset))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "environment_tests.rs"]
|
||||
mod tests;
|
||||
104
codex-rs/exec-server/src/secure_relay/environment_tests.rs
Normal file
104
codex-rs/exec-server/src/secure_relay/environment_tests.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::SinkExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use super::HarnessKeyValidator;
|
||||
use super::run_secure_multiplexed_environment;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::relay::encode_relay_message_frame;
|
||||
use crate::relay_proto::RelayMessageFrame;
|
||||
use crate::secure_channel::InitiatorHandshake;
|
||||
use crate::secure_channel::SecureChannelIdentity;
|
||||
use crate::secure_channel::SecureChannelPublicKey;
|
||||
use crate::secure_channel::secure_channel_prologue;
|
||||
use crate::server::ConnectionProcessor;
|
||||
|
||||
const ENVIRONMENT_ID: &str = "environment-1";
|
||||
const EXECUTOR_REGISTRATION_ID: &str = "registration-1";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BlockingValidator {
|
||||
calls: Arc<AtomicUsize>,
|
||||
release: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl HarnessKeyValidator for BlockingValidator {
|
||||
fn validate_harness_key(
|
||||
&self,
|
||||
_harness_public_key: &SecureChannelPublicKey,
|
||||
_authorization: &str,
|
||||
) -> impl std::future::Future<Output = Result<(), ExecServerError>> + Send {
|
||||
let calls = Arc::clone(&self.calls);
|
||||
let release = Arc::clone(&self.release);
|
||||
async move {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
release.notified().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_harness_key_validation_does_not_block_new_handshakes() -> Result<()> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
||||
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
||||
let (socket, _peer_addr) = listener.accept().await?;
|
||||
let environment_websocket = accept_async(socket).await?;
|
||||
let (mut harness_websocket, _response) = harness_connection.await??;
|
||||
|
||||
let environment_identity = SecureChannelIdentity::generate()?;
|
||||
let harness_identity = SecureChannelIdentity::generate()?;
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let environment_task = tokio::spawn(run_secure_multiplexed_environment(
|
||||
environment_websocket,
|
||||
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
||||
std::env::current_exe()?,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)?),
|
||||
ENVIRONMENT_ID.to_string(),
|
||||
EXECUTOR_REGISTRATION_ID.to_string(),
|
||||
environment_identity.clone(),
|
||||
BlockingValidator {
|
||||
calls: Arc::clone(&calls),
|
||||
release: Arc::new(Notify::new()),
|
||||
},
|
||||
));
|
||||
|
||||
for stream_id in ["stream-1", "stream-2"] {
|
||||
let prologue =
|
||||
secure_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id)?;
|
||||
let (_handshake, request) = InitiatorHandshake::start(
|
||||
&harness_identity,
|
||||
&environment_identity.public_key(),
|
||||
&prologue,
|
||||
b"authorization",
|
||||
)?;
|
||||
let frame = RelayMessageFrame::handshake(stream_id.to_string(), request);
|
||||
harness_websocket
|
||||
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
timeout(Duration::from_secs(1), async {
|
||||
while calls.load(Ordering::SeqCst) != 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
harness_websocket.close(None).await?;
|
||||
timeout(Duration::from_secs(1), environment_task).await??;
|
||||
Ok(())
|
||||
}
|
||||
402
codex-rs/exec-server/src/secure_relay/harness.rs
Normal file
402
codex-rs/exec-server/src/secure_relay/harness.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
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::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;
|
||||
use crate::secure_channel::InitiatorHandshake;
|
||||
use crate::secure_channel::SecureChannelIdentity;
|
||||
use crate::secure_channel::SecureChannelPublicKey;
|
||||
use crate::secure_channel::SecureTransport;
|
||||
use crate::secure_channel::secure_channel_prologue;
|
||||
use crate::secure_relay::message_framing::JsonRpcMessageDecoder;
|
||||
use crate::secure_relay::message_framing::SECURE_RECORD_PLAINTEXT_LEN;
|
||||
use crate::secure_relay::message_framing::frame_jsonrpc_message;
|
||||
use crate::secure_relay::ordered_ciphertext::OrderedCiphertextFrames;
|
||||
use crate::secure_relay::take_next_sequence;
|
||||
|
||||
/// 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.
|
||||
pub(crate) fn secure_harness_connection_from_websocket<T, E>(
|
||||
stream: T,
|
||||
connection_label: String,
|
||||
environment_id: String,
|
||||
executor_registration_id: String,
|
||||
identity: SecureChannelIdentity,
|
||||
responder_public_key: SecureChannelPublicKey,
|
||||
harness_key_authorization: String,
|
||||
) -> JsonRpcConnection
|
||||
where
|
||||
T: Sink<Message, Error = E> + Stream<Item = Result<Message, E>> + Unpin + Send + 'static,
|
||||
E: std::fmt::Display + Send + 'static,
|
||||
{
|
||||
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 =
|
||||
match secure_channel_prologue(&environment_id, &executor_registration_id, &stream_id) {
|
||||
Ok(prologue) => prologue,
|
||||
Err(error) => {
|
||||
send_disconnected(
|
||||
&incoming_tx,
|
||||
&disconnected_tx,
|
||||
format!("failed to build secure relay prologue: {error}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
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 secure 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,
|
||||
"secure relay websocket closed 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,
|
||||
"secure relay websocket closed during handshake".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_)) => continue,
|
||||
Ok(Message::Text(_)) => {
|
||||
send_disconnected(
|
||||
&incoming_tx,
|
||||
&disconnected_tx,
|
||||
"secure relay transport expects binary protobuf frames".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
send_disconnected(
|
||||
&incoming_tx,
|
||||
&disconnected_tx,
|
||||
format!(
|
||||
"failed to read secure 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 secure relay frame: {error}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
if frame.stream_id != stream_id {
|
||||
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, error.to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
match initiator_handshake.finish(&response) {
|
||||
Ok(transport) => break transport,
|
||||
Err(error) => {
|
||||
send_disconnected(
|
||||
&incoming_tx,
|
||||
&disconnected_tx,
|
||||
format!("secure relay handshake failed: {error}"),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(RelayFrameBodyKind::Reset) => {
|
||||
send_disconnected(
|
||||
&incoming_tx,
|
||||
&disconnected_tx,
|
||||
frame
|
||||
.into_reset_reason()
|
||||
.unwrap_or_else(|| "secure 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,
|
||||
"secure 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 secure relay: {error}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
for plaintext_record in framed.chunks(SECURE_RECORD_PLAINTEXT_LEN) {
|
||||
let seq = match take_next_sequence(&mut next_outbound_seq) {
|
||||
Ok(seq) => seq,
|
||||
Err(error) => {
|
||||
warn!("secure 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 secure relay: {error}");
|
||||
break 'relay;
|
||||
}
|
||||
};
|
||||
let frame = RelayMessageFrame::data(stream_id.clone(), seq, ciphertext);
|
||||
if websocket
|
||||
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
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,
|
||||
"secure 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,
|
||||
"secure relay transport expects binary protobuf frames".to_string(),
|
||||
)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
debug!("secure 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::External,
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_data(
|
||||
inbound_ciphertexts: &mut OrderedCiphertextFrames,
|
||||
transport: &mut SecureTransport,
|
||||
decoder: &mut JsonRpcMessageDecoder,
|
||||
data: RelayData,
|
||||
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
|
||||
) -> 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)?;
|
||||
|
||||
// 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<JsonRpcConnectionEvent>, reason: String) {
|
||||
let _ = incoming_tx
|
||||
.send(JsonRpcConnectionEvent::MalformedMessage { reason })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_disconnected(
|
||||
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
|
||||
disconnected_tx: &watch::Sender<bool>,
|
||||
reason: String,
|
||||
) {
|
||||
let _ = disconnected_tx.send(true);
|
||||
let _ = incoming_tx
|
||||
.send(JsonRpcConnectionEvent::Disconnected {
|
||||
reason: Some(reason),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
92
codex-rs/exec-server/src/secure_relay/message_framing.rs
Normal file
92
codex-rs/exec-server/src/secure_relay/message_framing.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
|
||||
use crate::ExecServerError;
|
||||
|
||||
const LENGTH_PREFIX_BYTES: usize = size_of::<u32>();
|
||||
const MAX_SECURE_JSONRPC_MESSAGE_LEN: usize = 64 * 1024 * 1024;
|
||||
pub(crate) const SECURE_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<Vec<u8>, 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_SECURE_JSONRPC_MESSAGE_LEN {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure 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.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct JsonRpcMessageDecoder {
|
||||
buffered: Vec<u8>,
|
||||
}
|
||||
|
||||
impl JsonRpcMessageDecoder {
|
||||
/// Append one decrypted record and return all complete framed messages.
|
||||
pub(crate) fn push(
|
||||
&mut self,
|
||||
plaintext_record: &[u8],
|
||||
) -> Result<Vec<JSONRPCMessage>, ExecServerError> {
|
||||
if plaintext_record.len() > SECURE_RECORD_PLAINTEXT_LEN {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure 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();
|
||||
loop {
|
||||
let Some(message_len) = self.next_message_len()? else {
|
||||
break;
|
||||
};
|
||||
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_SECURE_JSONRPC_MESSAGE_LEN {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure relay JSON-RPC reassembly buffer exceeds maximum length".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
fn next_message_len(&self) -> Result<Option<usize>, ExecServerError> {
|
||||
let Some(prefix) = self.buffered.get(..LENGTH_PREFIX_BYTES) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let message_len = u32::from_be_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]) as usize;
|
||||
// Zero-length is never a JSON-RPC message, and rejecting an oversized
|
||||
// declaration immediately avoids waiting for attacker-controlled data.
|
||||
if message_len == 0 || message_len > MAX_SECURE_JSONRPC_MESSAGE_LEN {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure relay JSON-RPC message has invalid length".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Some(message_len))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "message_framing_tests.rs"]
|
||||
mod tests;
|
||||
@@ -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_SECURE_JSONRPC_MESSAGE_LEN;
|
||||
use super::SECURE_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(SECURE_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_SECURE_JSONRPC_MESSAGE_LEN as u32 + 1).to_be_bytes();
|
||||
|
||||
assert!(matches!(
|
||||
decoder.push(&declared_len),
|
||||
Err(ExecServerError::Protocol(message))
|
||||
if message == "secure relay JSON-RPC message has invalid length"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversized_plaintext_record() {
|
||||
let mut decoder = JsonRpcMessageDecoder::default();
|
||||
|
||||
assert!(matches!(
|
||||
decoder.push(&vec![0; SECURE_RECORD_PLAINTEXT_LEN + 1]),
|
||||
Err(ExecServerError::Protocol(message))
|
||||
if message == "secure relay plaintext record exceeds maximum length"
|
||||
));
|
||||
}
|
||||
33
codex-rs/exec-server/src/secure_relay/mod.rs
Normal file
33
codex-rs/exec-server/src/secure_relay/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
mod environment;
|
||||
mod harness;
|
||||
mod message_framing;
|
||||
mod ordered_ciphertext;
|
||||
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
|
||||
use crate::ExecServerError;
|
||||
|
||||
pub(crate) use environment::HarnessKeyValidator;
|
||||
pub(crate) use environment::run_secure_multiplexed_environment;
|
||||
pub(crate) use harness::secure_harness_connection_from_websocket;
|
||||
|
||||
// This bounds allocation in tungstenite before protobuf and secure-record
|
||||
// validation run. It comfortably fits one maximum Noise record plus metadata.
|
||||
const MAX_SECURE_RELAY_WEBSOCKET_MESSAGE_SIZE: usize = 256 * 1024;
|
||||
|
||||
/// Return the websocket limits required by every secure relay endpoint.
|
||||
pub(crate) fn secure_relay_websocket_config() -> WebSocketConfig {
|
||||
WebSocketConfig::default()
|
||||
.max_frame_size(Some(MAX_SECURE_RELAY_WEBSOCKET_MESSAGE_SIZE))
|
||||
.max_message_size(Some(MAX_SECURE_RELAY_WEBSOCKET_MESSAGE_SIZE))
|
||||
}
|
||||
|
||||
fn take_next_sequence(next_seq: &mut u32) -> Result<u32, ExecServerError> {
|
||||
// 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("secure relay sequence number exhausted".to_string())
|
||||
})?;
|
||||
Ok(seq)
|
||||
}
|
||||
85
codex-rs/exec-server/src/secure_relay/ordered_ciphertext.rs
Normal file
85
codex-rs/exec-server/src/secure_relay/ordered_ciphertext.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::ExecServerError;
|
||||
|
||||
const MAX_REORDER_DISTANCE: u32 = 64;
|
||||
const MAX_PENDING_FRAMES: usize = 64;
|
||||
const MAX_PENDING_BYTES: usize = 1024 * 1024;
|
||||
|
||||
/// Bounded pre-decryption reorder buffer for Noise transport records.
|
||||
///
|
||||
/// Relay delivery can be duplicated or reordered, but Noise transport nonces
|
||||
/// are strictly ordered. This type absorbs only a small reliable-delivery
|
||||
/// window and releases ciphertexts exactly once in nonce order. It must sit
|
||||
/// before `SecureTransport::decrypt`; attempting to decrypt a future record
|
||||
/// would advance or desynchronize cryptographic state.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct OrderedCiphertextFrames {
|
||||
next_seq: u32,
|
||||
pending: BTreeMap<u32, Vec<u8>>,
|
||||
pending_bytes: usize,
|
||||
}
|
||||
|
||||
impl OrderedCiphertextFrames {
|
||||
/// Accept one relay record and return the newly contiguous ciphertext run.
|
||||
pub(crate) fn push(
|
||||
&mut self,
|
||||
seq: u32,
|
||||
payload: Vec<u8>,
|
||||
) -> Result<Vec<Vec<u8>>, ExecServerError> {
|
||||
// Already-delivered and already-buffered frames are retries. Keep the
|
||||
// first buffered ciphertext for a sequence so a duplicate cannot
|
||||
// replace it before authentication.
|
||||
if seq < self.next_seq || self.pending.contains_key(&seq) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if seq > self.next_seq {
|
||||
// Bound both sequence distance and actual buffered memory. Without
|
||||
// both limits, an authenticated peer could hold the stream open
|
||||
// while forcing unbounded pre-decryption state.
|
||||
if seq - self.next_seq > MAX_REORDER_DISTANCE {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure relay ciphertext exceeds reorder window".to_string(),
|
||||
));
|
||||
}
|
||||
let pending_bytes = self
|
||||
.pending_bytes
|
||||
.checked_add(payload.len())
|
||||
.ok_or_else(|| {
|
||||
ExecServerError::Protocol(
|
||||
"secure relay pending ciphertext byte count overflowed".to_string(),
|
||||
)
|
||||
})?;
|
||||
if self.pending.len() >= MAX_PENDING_FRAMES || pending_bytes > MAX_PENDING_BYTES {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"secure relay pending ciphertext buffer is full".to_string(),
|
||||
));
|
||||
}
|
||||
self.pending.insert(seq, payload);
|
||||
self.pending_bytes = pending_bytes;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// The expected record closes the current gap. Release it and every
|
||||
// contiguous buffered successor so Noise sees exactly nonce order.
|
||||
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("secure relay sequence number exhausted".to_string())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ordered_ciphertext_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,48 @@
|
||||
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(1, b"second".to_vec()).unwrap(),
|
||||
Vec::<Vec<u8>>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
frames.push(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(1, b"first copy".to_vec()).unwrap(),
|
||||
Vec::<Vec<u8>>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
frames.push(1, b"replacement".to_vec()).unwrap(),
|
||||
Vec::<Vec<u8>>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
frames.push(0, b"zero".to_vec()).unwrap(),
|
||||
vec![b"zero".to_vec(), b"first copy".to_vec()]
|
||||
);
|
||||
assert_eq!(
|
||||
frames.push(0, b"duplicate".to_vec()).unwrap(),
|
||||
Vec::<Vec<u8>>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unbounded_reordering() {
|
||||
let mut frames = OrderedCiphertextFrames::default();
|
||||
|
||||
assert!(frames.push(65, Vec::new()).is_err());
|
||||
assert!(frames.push(1, vec![0; MAX_PENDING_BYTES + 1]).is_err());
|
||||
}
|
||||
@@ -4,40 +4,49 @@ mod common;
|
||||
mod relay_proto;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_api::AuthProvider;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecResponse;
|
||||
use codex_exec_server::ExecServerClient;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_exec_server::InitializeParams;
|
||||
use codex_exec_server::InitializeResponse;
|
||||
use codex_exec_server::FsReadFileParams;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::RemoteEnvironmentConfig;
|
||||
use codex_exec_server::SecureChannelIdentity;
|
||||
use codex_exec_server::SecureChannelPublicKey;
|
||||
use codex_exec_server::SecureRendezvousConnectArgs;
|
||||
use codex_exec_server::SecureRendezvousConnectBundle;
|
||||
use codex_exec_server::SecureRendezvousConnectProvider;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use futures::FutureExt;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use pretty_assertions::assert_eq;
|
||||
use prost::Message as ProstMessage;
|
||||
use relay_proto::RelayData;
|
||||
use relay_proto::RelayMessageFrame;
|
||||
use relay_proto::RelayReset;
|
||||
use relay_proto::relay_message_frame;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use uuid::Uuid;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
@@ -45,10 +54,11 @@ use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const ENVIRONMENT_ID: &str = "env-mux-test";
|
||||
const ENVIRONMENT_ID: &str = "env-secure-relay-test";
|
||||
const EXECUTOR_REGISTRATION_ID: &str = "registration-1";
|
||||
const HARNESS_KEY_AUTHORIZATION: &str = "harness-key-authorization";
|
||||
const REGISTRY_TOKEN: &str = "registry-token";
|
||||
const RELAY_MESSAGE_FRAME_VERSION: u32 = 1;
|
||||
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const TEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StaticRegistryAuthProvider;
|
||||
@@ -62,12 +72,135 @@ impl AuthProvider for StaticRegistryAuthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
struct FailingSecureConnectProvider {
|
||||
attempts: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl SecureRendezvousConnectProvider for FailingSecureConnectProvider {
|
||||
fn environment_id(&self) -> &str {
|
||||
ENVIRONMENT_ID
|
||||
}
|
||||
|
||||
fn connect_args(&self) -> BoxFuture<'_, Result<SecureRendezvousConnectArgs, ExecServerError>> {
|
||||
self.attempts.fetch_add(1, Ordering::SeqCst);
|
||||
async {
|
||||
Err(ExecServerError::Protocol(
|
||||
"test registry connect failure".to_string(),
|
||||
))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
struct WrongEnvironmentSecureConnectProvider {
|
||||
harness_identity: SecureChannelIdentity,
|
||||
executor_public_key: SecureChannelPublicKey,
|
||||
}
|
||||
|
||||
impl SecureRendezvousConnectProvider for WrongEnvironmentSecureConnectProvider {
|
||||
fn environment_id(&self) -> &str {
|
||||
ENVIRONMENT_ID
|
||||
}
|
||||
|
||||
fn connect_args(&self) -> BoxFuture<'_, Result<SecureRendezvousConnectArgs, ExecServerError>> {
|
||||
async move {
|
||||
Ok(SecureRendezvousConnectArgs::new(
|
||||
SecureRendezvousConnectBundle {
|
||||
websocket_url: "ws://127.0.0.1:1".to_string(),
|
||||
environment_id: "wrong-environment".to_string(),
|
||||
executor_registration_id: EXECUTOR_REGISTRATION_ID.to_string(),
|
||||
executor_public_key: self.executor_public_key.clone(),
|
||||
harness_key_authorization: HARNESS_KEY_AUTHORIZATION.to_string(),
|
||||
},
|
||||
self.harness_identity.clone(),
|
||||
"secure-relay-test".to_string(),
|
||||
))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn static_registry_auth_provider() -> codex_api::SharedAuthProvider {
|
||||
Arc::new(StaticRegistryAuthProvider)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multiplexed_remote_environment_routes_independent_virtual_streams() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn secure_environment_refreshes_bundle_for_each_connection_attempt() -> Result<()> {
|
||||
let attempts = Arc::new(AtomicUsize::new(0));
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
manager.upsert_secure_environment(
|
||||
ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(FailingSecureConnectProvider {
|
||||
attempts: Arc::clone(&attempts),
|
||||
}),
|
||||
)?;
|
||||
let backend = manager
|
||||
.get_environment(ENVIRONMENT_ID)
|
||||
.context("secure environment should be materialized")?
|
||||
.get_exec_backend();
|
||||
|
||||
for attempt in 1..=2 {
|
||||
let result = backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::new(format!("proc-{attempt}")),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir()?,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
})
|
||||
.await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ExecServerError::Protocol(ref message))
|
||||
if message == "test registry connect failure"
|
||||
));
|
||||
}
|
||||
|
||||
assert_eq!(attempts.load(Ordering::SeqCst), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn secure_environment_rejects_provider_bundle_for_another_environment() -> Result<()> {
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
manager.upsert_secure_environment(
|
||||
ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(WrongEnvironmentSecureConnectProvider {
|
||||
harness_identity: SecureChannelIdentity::generate()?,
|
||||
executor_public_key: SecureChannelIdentity::generate()?.public_key(),
|
||||
}),
|
||||
)?;
|
||||
let backend = manager
|
||||
.get_environment(ENVIRONMENT_ID)
|
||||
.context("secure environment should be materialized")?
|
||||
.get_exec_backend();
|
||||
|
||||
let result = backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-wrong-environment"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir()?,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ExecServerError::Protocol(ref message))
|
||||
if message == "secure rendezvous provider returned a different environment id"
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let rendezvous_url = format!("ws://{}", listener.local_addr()?);
|
||||
let registry = MockServer::start().await;
|
||||
@@ -78,10 +211,20 @@ async fn multiplexed_remote_environment_routes_independent_virtual_streams() ->
|
||||
.and(header("authorization", format!("Bearer {REGISTRY_TOKEN}")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"environment_id": ENVIRONMENT_ID,
|
||||
"url": rendezvous_url,
|
||||
"url": format!("{rendezvous_url}/relay?role=environment"),
|
||||
"security_profile": "noise_hybrid_ik_v1",
|
||||
"executor_registration_id": EXECUTOR_REGISTRATION_ID,
|
||||
})))
|
||||
.mount(®istry)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/cloud/environment/{ENVIRONMENT_ID}/validate-harness-key"
|
||||
)))
|
||||
.and(header("authorization", format!("Bearer {REGISTRY_TOKEN}")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
|
||||
.mount(®istry)
|
||||
.await;
|
||||
|
||||
let (codex_exe, codex_linux_sandbox_exe) = common::current_test_binary_helper_paths()?;
|
||||
let runtime_paths = ExecServerRuntimePaths::new(codex_exe, codex_linux_sandbox_exe)?;
|
||||
@@ -95,281 +238,160 @@ async fn multiplexed_remote_environment_routes_independent_virtual_streams() ->
|
||||
runtime_paths,
|
||||
));
|
||||
|
||||
let (socket, _peer_addr) = timeout(TEST_TIMEOUT, listener.accept())
|
||||
let environment_websocket = accept_websocket(&listener, "environment").await?;
|
||||
let executor_public_key = registered_executor_public_key(®istry).await?;
|
||||
let harness_identity = SecureChannelIdentity::generate()?;
|
||||
let client_args = SecureRendezvousConnectArgs::new(
|
||||
SecureRendezvousConnectBundle {
|
||||
websocket_url: format!("{rendezvous_url}/relay?role=harness"),
|
||||
environment_id: ENVIRONMENT_ID.to_string(),
|
||||
executor_registration_id: EXECUTOR_REGISTRATION_ID.to_string(),
|
||||
executor_public_key,
|
||||
harness_key_authorization: HARNESS_KEY_AUTHORIZATION.to_string(),
|
||||
},
|
||||
harness_identity,
|
||||
"secure-relay-test".to_string(),
|
||||
);
|
||||
let client_task =
|
||||
tokio::spawn(async move { ExecServerClient::connect_secure_rendezvous(client_args).await });
|
||||
let harness_websocket = accept_websocket(&listener, "harness").await?;
|
||||
let captured_frames = Arc::new(Mutex::new(Vec::new()));
|
||||
let relay_task = tokio::spawn(proxy_relay_frames(
|
||||
environment_websocket,
|
||||
harness_websocket,
|
||||
Arc::clone(&captured_frames),
|
||||
));
|
||||
let client = timeout(TEST_TIMEOUT, client_task)
|
||||
.await
|
||||
.context("remote environment should connect to fake rendezvous")??;
|
||||
let mut websocket = timeout(TEST_TIMEOUT, accept_async(socket))
|
||||
.await
|
||||
.context("fake rendezvous should accept environment websocket")??;
|
||||
.context("secure harness client should connect")???;
|
||||
|
||||
let stream_a = "stream-a";
|
||||
let stream_b = "stream-b";
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_a,
|
||||
/*seq*/ 0,
|
||||
initialize_request(/*id*/ 1, "relay-test-a")?,
|
||||
)
|
||||
.await?;
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_b,
|
||||
/*seq*/ 0,
|
||||
initialize_request(/*id*/ 1, "relay-test-b")?,
|
||||
)
|
||||
.await?;
|
||||
let response = client
|
||||
.exec(ExecParams {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir()?,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-1")
|
||||
}
|
||||
);
|
||||
|
||||
let initialize_responses = read_relay_messages_by_stream(&mut websocket, /*count*/ 2).await?;
|
||||
let session_a =
|
||||
assert_initialize_response(initialize_responses.get(stream_a), stream_a, /*id*/ 1)?;
|
||||
let session_b =
|
||||
assert_initialize_response(initialize_responses.get(stream_b), stream_b, /*id*/ 1)?;
|
||||
assert_ne!(session_a, session_b);
|
||||
let temp_dir = TempDir::new()?;
|
||||
let large_file_path = temp_dir.path().join("large-response.bin");
|
||||
let large_file_contents = vec![0x5a; 128 * 1024];
|
||||
std::fs::write(&large_file_path, &large_file_contents)?;
|
||||
let read_response = client
|
||||
.fs_read_file(FsReadFileParams {
|
||||
path: AbsolutePathBuf::try_from(large_file_path)?,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
STANDARD.decode(read_response.data_base64)?,
|
||||
large_file_contents
|
||||
);
|
||||
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_a,
|
||||
/*seq*/ 1,
|
||||
notification("initialized", serde_json::json!({})),
|
||||
)
|
||||
.await?;
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_b,
|
||||
/*seq*/ 1,
|
||||
notification("initialized", serde_json::json!({})),
|
||||
)
|
||||
.await?;
|
||||
assert_relay_data_is_encrypted(&captured_frames)?;
|
||||
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_a,
|
||||
/*seq*/ 2,
|
||||
request(/*id*/ 2, "test/unknown-a", serde_json::json!({})),
|
||||
)
|
||||
.await?;
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_b,
|
||||
/*seq*/ 2,
|
||||
request(/*id*/ 2, "test/unknown-b", serde_json::json!({})),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let unknown_method_responses =
|
||||
read_relay_messages_by_stream(&mut websocket, /*count*/ 2).await?;
|
||||
assert_error_response(
|
||||
unknown_method_responses.get(stream_a),
|
||||
stream_a,
|
||||
/*id*/ 2,
|
||||
"test/unknown-a",
|
||||
)?;
|
||||
assert_error_response(
|
||||
unknown_method_responses.get(stream_b),
|
||||
stream_b,
|
||||
/*id*/ 2,
|
||||
"test/unknown-b",
|
||||
)?;
|
||||
|
||||
send_relay_reset(&mut websocket, stream_a, "test_reset").await?;
|
||||
send_relay_message(
|
||||
&mut websocket,
|
||||
stream_b,
|
||||
/*seq*/ 3,
|
||||
request(
|
||||
/*id*/ 3,
|
||||
"test/unknown-b-after-reset",
|
||||
serde_json::json!({}),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (stream_id, message) = read_relay_message(&mut websocket).await?;
|
||||
assert_eq!(stream_id, stream_b);
|
||||
assert_error_response(
|
||||
Some(&message),
|
||||
stream_b,
|
||||
/*id*/ 3,
|
||||
"test/unknown-b-after-reset",
|
||||
)?;
|
||||
|
||||
websocket.close(None).await?;
|
||||
drop(client);
|
||||
relay_task.abort();
|
||||
remote_environment.abort();
|
||||
let _ = relay_task.await;
|
||||
let _ = remote_environment.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_relay_message<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
stream_id: &str,
|
||||
seq: u32,
|
||||
message: JSONRPCMessage,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
let payload = serde_json::to_vec(&message)?;
|
||||
let frame = RelayMessageFrame {
|
||||
version: RELAY_MESSAGE_FRAME_VERSION,
|
||||
stream_id: stream_id.to_string(),
|
||||
ack: 0,
|
||||
ack_bits: 0,
|
||||
body: Some(relay_message_frame::Body::Data(RelayData {
|
||||
seq,
|
||||
segment_index: 0,
|
||||
segment_count: 1,
|
||||
payload,
|
||||
})),
|
||||
};
|
||||
send_relay_frame(websocket, frame).await
|
||||
async fn accept_websocket(
|
||||
listener: &TcpListener,
|
||||
role: &str,
|
||||
) -> Result<WebSocketStream<TcpStream>> {
|
||||
let (socket, _peer_addr) = timeout(TEST_TIMEOUT, listener.accept())
|
||||
.await
|
||||
.with_context(|| format!("remote {role} should connect to fake rendezvous"))??;
|
||||
timeout(TEST_TIMEOUT, accept_async(socket))
|
||||
.await
|
||||
.with_context(|| format!("fake rendezvous should accept {role} websocket"))?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn send_relay_reset<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
stream_id: &str,
|
||||
reason: &str,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
send_relay_frame(
|
||||
websocket,
|
||||
RelayMessageFrame {
|
||||
version: RELAY_MESSAGE_FRAME_VERSION,
|
||||
stream_id: stream_id.to_string(),
|
||||
ack: 0,
|
||||
ack_bits: 0,
|
||||
body: Some(relay_message_frame::Body::Reset(RelayReset {
|
||||
reason: reason.to_string(),
|
||||
})),
|
||||
},
|
||||
)
|
||||
.await
|
||||
async fn registered_executor_public_key(registry: &MockServer) -> Result<SecureChannelPublicKey> {
|
||||
let requests = registry
|
||||
.received_requests()
|
||||
.await
|
||||
.context("wiremock should retain requests")?;
|
||||
let request = requests
|
||||
.iter()
|
||||
.find(|request| request.url.path().ends_with("/register"))
|
||||
.context("exec-server should register before connecting")?;
|
||||
let body: serde_json::Value = serde_json::from_slice(&request.body)?;
|
||||
let key = serde_json::from_value(body["executor_public_key"].clone())?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
async fn send_relay_frame<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
frame: RelayMessageFrame,
|
||||
) -> Result<()>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
websocket
|
||||
.send(Message::Binary(frame.encode_to_vec().into()))
|
||||
.await?;
|
||||
async fn proxy_relay_frames(
|
||||
mut environment: WebSocketStream<TcpStream>,
|
||||
mut harness: WebSocketStream<TcpStream>,
|
||||
captured_frames: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
message = environment.next() => {
|
||||
let Some(message) = message else {
|
||||
break;
|
||||
};
|
||||
let message = message?;
|
||||
capture_binary_frame(&captured_frames, &message);
|
||||
harness.send(message).await?;
|
||||
}
|
||||
message = harness.next() => {
|
||||
let Some(message) = message else {
|
||||
break;
|
||||
};
|
||||
let message = message?;
|
||||
capture_binary_frame(&captured_frames, &message);
|
||||
environment.send(message).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_relay_messages_by_stream<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
count: usize,
|
||||
) -> Result<HashMap<String, JSONRPCMessage>>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
let mut messages = HashMap::new();
|
||||
for _ in 0..count {
|
||||
let (stream_id, message) = read_relay_message(websocket).await?;
|
||||
if messages.insert(stream_id.clone(), message).is_some() {
|
||||
bail!("received duplicate response for stream {stream_id}");
|
||||
}
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
async fn read_relay_message<S>(
|
||||
websocket: &mut WebSocketStream<S>,
|
||||
) -> Result<(String, JSONRPCMessage)>
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
loop {
|
||||
let frame = timeout(TEST_TIMEOUT, websocket.next())
|
||||
.await
|
||||
.context("timed out waiting for relay frame")?
|
||||
.ok_or_else(|| anyhow!("environment websocket closed"))??;
|
||||
match frame {
|
||||
Message::Binary(bytes) => {
|
||||
let frame = RelayMessageFrame::decode(bytes.as_ref())?;
|
||||
let stream_id = frame.stream_id;
|
||||
let Some(relay_message_frame::Body::Data(data)) = frame.body else {
|
||||
continue;
|
||||
};
|
||||
let message = serde_json::from_slice(&data.payload)?;
|
||||
return Ok((stream_id, message));
|
||||
}
|
||||
Message::Ping(_) | Message::Pong(_) => {}
|
||||
Message::Close(_) => bail!("environment websocket closed"),
|
||||
Message::Text(_) => bail!("environment sent text frame on relay websocket"),
|
||||
Message::Frame(_) => {}
|
||||
}
|
||||
fn capture_binary_frame(captured_frames: &Mutex<Vec<Vec<u8>>>, message: &Message) {
|
||||
if let Message::Binary(bytes) = message {
|
||||
captured_frames
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(bytes.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_request(id: i64, client_name: &str) -> Result<JSONRPCMessage> {
|
||||
Ok(request(
|
||||
id,
|
||||
"initialize",
|
||||
serde_json::to_value(InitializeParams {
|
||||
client_name: client_name.to_string(),
|
||||
resume_session_id: None,
|
||||
})?,
|
||||
))
|
||||
}
|
||||
|
||||
fn request(id: i64, method: &str, params: serde_json::Value) -> JSONRPCMessage {
|
||||
JSONRPCMessage::Request(JSONRPCRequest {
|
||||
id: RequestId::Integer(id),
|
||||
method: method.to_string(),
|
||||
params: Some(params),
|
||||
trace: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn notification(method: &str, params: serde_json::Value) -> JSONRPCMessage {
|
||||
JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: method.to_string(),
|
||||
params: Some(params),
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_initialize_response(
|
||||
message: Option<&JSONRPCMessage>,
|
||||
stream_id: &str,
|
||||
id: i64,
|
||||
) -> Result<Uuid> {
|
||||
let message = message.ok_or_else(|| anyhow!("missing initialize response for {stream_id}"))?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: response_id,
|
||||
result,
|
||||
}) = message
|
||||
else {
|
||||
bail!("expected initialize response for {stream_id}, got {message:?}");
|
||||
};
|
||||
assert_eq!(response_id, &RequestId::Integer(id));
|
||||
let response: InitializeResponse = serde_json::from_value(result.clone())?;
|
||||
Ok(Uuid::parse_str(&response.session_id)?)
|
||||
}
|
||||
|
||||
fn assert_error_response(
|
||||
message: Option<&JSONRPCMessage>,
|
||||
stream_id: &str,
|
||||
id: i64,
|
||||
expected_method: &str,
|
||||
) -> Result<()> {
|
||||
let message = message.ok_or_else(|| anyhow!("missing error response for {stream_id}"))?;
|
||||
let JSONRPCMessage::Error(JSONRPCError {
|
||||
id: response_id,
|
||||
error,
|
||||
}) = message
|
||||
else {
|
||||
bail!("expected error response for {stream_id}, got {message:?}");
|
||||
};
|
||||
assert_eq!(response_id, &RequestId::Integer(id));
|
||||
fn assert_relay_data_is_encrypted(captured_frames: &Mutex<Vec<Vec<u8>>>) -> Result<()> {
|
||||
let captured_frames = captured_frames
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut data_frames = 0;
|
||||
for encoded in captured_frames.iter() {
|
||||
let frame = RelayMessageFrame::decode(encoded.as_slice())?;
|
||||
let Some(relay_message_frame::Body::Data(data)) = frame.body else {
|
||||
continue;
|
||||
};
|
||||
data_frames += 1;
|
||||
let payload = String::from_utf8_lossy(&data.payload);
|
||||
assert!(!payload.contains("initialize"));
|
||||
assert!(!payload.contains("process/start"));
|
||||
assert!(!payload.contains("secure-relay-test"));
|
||||
}
|
||||
assert!(
|
||||
error.message.contains(expected_method),
|
||||
"expected error for {stream_id} to mention {expected_method}, got {}",
|
||||
error.message
|
||||
data_frames >= 4,
|
||||
"expected encrypted request and response frames"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user