diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 1dbc873796..c107884e1d 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -667,6 +667,7 @@ "axum_0.7.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"axum-core\",\"req\":\"^0.4.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.4.2\"},{\"features\":[\"__private\"],\"kind\":\"dev\",\"name\":\"axum-macros\",\"req\":\"^0.4.1\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"^0.7\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.24.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.24.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:serde_urlencoded\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:serde_urlencoded\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", + "base16ct_0.2.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "base64_0.21.7": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64_0.22.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64ct_1.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.6\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", @@ -774,6 +775,7 @@ "crossbeam-utils_0.8.21": "{\"dependencies\":[{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", "crossterm_winapi_0.9.1": "{\"dependencies\":[{\"features\":[\"winbase\",\"consoleapi\",\"processenv\",\"handleapi\",\"synchapi\",\"impl-default\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", + "crypto-bigint_0.5.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"rlp\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"serdect?/alloc\"],\"default\":[\"rand\"],\"extra-sizes\":[],\"rand\":[\"rand_core/std\"],\"serde\":[\"dep:serdect\"]}}", "crypto-common_0.1.7": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"=0.14.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", "crypto_box_0.9.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"blake2\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"crypto_secretbox\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rmp-serde\",\"req\":\"^1\"},{\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"chacha20\":[\"dep:chacha20\",\"crypto_secretbox/chacha20\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"salsa20\":[\"dep:salsa20\",\"crypto_secretbox/salsa20\"],\"seal\":[\"dep:blake2\",\"alloc\"],\"serde\":[\"dep:serdect\"],\"std\":[\"aead/std\"]}}", "crypto_secretbox_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"cipher\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"features\":[\"zeroize\"],\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", @@ -846,9 +848,11 @@ "dylint_linting_5.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_cmd\",\"req\":\"^2.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"features\":[\"config\"],\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.23\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"name\":\"toml\",\"req\":\"^0.9\"},{\"kind\":\"build\",\"name\":\"toml\",\"req\":\"^0.9\"}],\"features\":{\"constituent\":[]}}", "dylint_testing_5.0.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"name\":\"compiletest_rs\",\"req\":\"^0.11\"},{\"name\":\"dylint\",\"req\":\"=5.0.0\"},{\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"regex\",\"req\":\"^1.11\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"tempfile\",\"req\":\"^3.23\"}],\"features\":{\"default\":[],\"deny_warnings\":[]}}", "dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}", + "ecdsa_0.16.9": "{\"dependencies\":[{\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10.7\"},{\"default_features\":false,\"features\":[\"digest\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.6\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"elliptic-curve\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"rfc6979\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"signature\",\"req\":\"^2.0, <2.3\"},{\"default_features\":false,\"name\":\"spki\",\"optional\":true,\"req\":\"^0.7.2\"}],\"features\":{\"alloc\":[\"elliptic-curve/alloc\",\"signature/alloc\",\"spki/alloc\"],\"arithmetic\":[\"elliptic-curve/arithmetic\"],\"default\":[\"digest\"],\"dev\":[\"arithmetic\",\"digest\",\"elliptic-curve/dev\",\"hazmat\"],\"digest\":[\"dep:digest\",\"signature/digest\"],\"hazmat\":[],\"pem\":[\"elliptic-curve/pem\",\"pkcs8\"],\"pkcs8\":[\"digest\",\"elliptic-curve/pkcs8\",\"der\"],\"serde\":[\"elliptic-curve/serde\",\"serdect\"],\"signing\":[\"arithmetic\",\"digest\",\"hazmat\",\"rfc6979\"],\"std\":[\"alloc\",\"elliptic-curve/std\",\"signature/std\"],\"verifying\":[\"arithmetic\",\"digest\",\"hazmat\"]}}", "ed25519-dalek_2.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"blake2\",\"req\":\"^0.10\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"digest\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"features\":[\"digest\",\"rand_core\"],\"kind\":\"dev\",\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"ed25519\",\"req\":\">=2.2, <2.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"merlin\",\"optional\":true,\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"signature\",\"optional\":true,\"req\":\">=2.0, <2.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"static_secrets\"],\"kind\":\"dev\",\"name\":\"x25519-dalek\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"ed25519/alloc\",\"serde?/alloc\",\"zeroize/alloc\"],\"asm\":[\"sha2/asm\"],\"batch\":[\"alloc\",\"merlin\",\"rand_core\"],\"default\":[\"fast\",\"std\",\"zeroize\"],\"digest\":[\"signature/digest\"],\"fast\":[\"curve25519-dalek/precomputed-tables\"],\"hazmat\":[],\"legacy_compatibility\":[\"curve25519-dalek/legacy_compatibility\"],\"pem\":[\"alloc\",\"ed25519/pem\",\"pkcs8\"],\"pkcs8\":[\"ed25519/pkcs8\"],\"rand_core\":[\"dep:rand_core\"],\"serde\":[\"dep:serde\",\"ed25519/serde\"],\"std\":[\"alloc\",\"ed25519/std\",\"serde?/std\",\"sha2/std\"],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", "ed25519_2.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"ed25519-dalek\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"signature\"],\"kind\":\"dev\",\"name\":\"ring-compat\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"serde_bytes\",\"optional\":true,\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"signature\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"pkcs8?/alloc\"],\"default\":[\"std\"],\"pem\":[\"alloc\",\"pkcs8/pem\"],\"serde_bytes\":[\"serde\",\"dep:serde_bytes\"],\"std\":[\"pkcs8?/std\",\"signature/std\"]}}", "either_1.15.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[],\"use_std\":[\"std\"]}}", + "elliptic-curve_0.13.8": "{\"dependencies\":[{\"name\":\"base16ct\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64ct\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"rand_core\",\"generic-array\",\"zeroize\"],\"name\":\"crypto-bigint\",\"req\":\"^0.5\"},{\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.6\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.1\"},{\"features\":[\"alloc\"],\"name\":\"pem-rfc7468\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"features\":[\"subtle\",\"zeroize\"],\"name\":\"sec1\",\"optional\":true,\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.47\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"tap\",\"optional\":true,\"req\":\"^1.0.1\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.7\"}],\"features\":{\"alloc\":[\"base16ct/alloc\",\"ff?/alloc\",\"group?/alloc\",\"pkcs8?/alloc\",\"sec1?/alloc\",\"zeroize/alloc\"],\"arithmetic\":[\"group\"],\"bits\":[\"arithmetic\",\"ff/bits\",\"dep:tap\"],\"default\":[\"arithmetic\"],\"dev\":[\"arithmetic\",\"dep:hex-literal\",\"pem\",\"pkcs8\"],\"ecdh\":[\"arithmetic\",\"digest\",\"dep:hkdf\"],\"group\":[\"dep:group\",\"ff\"],\"hash2curve\":[\"arithmetic\",\"digest\"],\"hazmat\":[],\"jwk\":[\"dep:base64ct\",\"dep:serde_json\",\"alloc\",\"serde\",\"zeroize/alloc\"],\"pem\":[\"dep:pem-rfc7468\",\"alloc\",\"arithmetic\",\"pkcs8\",\"sec1/pem\"],\"pkcs8\":[\"dep:pkcs8\",\"sec1\"],\"serde\":[\"dep:serdect\",\"alloc\",\"pkcs8\",\"sec1/serde\"],\"std\":[\"alloc\",\"rand_core/std\",\"pkcs8?/std\",\"sec1?/std\"],\"voprf\":[\"digest\"]}}", "ena_0.14.3": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}", "encode_unicode_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"features\":[\"https-native\"],\"kind\":\"dev\",\"name\":\"minreq\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "encoding_rs_0.8.35": "{\"dependencies\":[{\"name\":\"any_all_workaround\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"fast-big5-hanzi-encode\":[],\"fast-gb-hanzi-encode\":[],\"fast-hangul-encode\":[],\"fast-hanja-encode\":[],\"fast-kanji-encode\":[],\"fast-legacy-encode\":[\"fast-hangul-encode\",\"fast-hanja-encode\",\"fast-kanji-encode\",\"fast-gb-hanzi-encode\",\"fast-big5-hanzi-encode\"],\"less-slow-big5-hanzi-encode\":[],\"less-slow-gb-hanzi-encode\":[],\"less-slow-kanji-encode\":[],\"simd-accel\":[\"any_all_workaround\"]}}", @@ -879,6 +883,7 @@ "fax_derive_0.2.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "fd-lock_4.0.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.0.8\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "fdeflate_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"simd-adler32\",\"req\":\"^0.3.4\"}],\"features\":{}}", + "ff_0.13.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"blake2b_simd\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"ff_derive\",\"optional\":true,\"req\":\"^0.13.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"subtle\",\"req\":\"^2.2.1\"}],\"features\":{\"alloc\":[],\"bits\":[\"bitvec\"],\"default\":[\"bits\",\"std\"],\"derive\":[\"byteorder\",\"ff_derive\"],\"derive_bits\":[\"bits\",\"ff_derive/bits\"],\"std\":[\"alloc\"]}}", "fiat-crypto_0.2.9": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "filedescriptor_0.8.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"winuser\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"processthreadsapi\",\"winsock2\",\"processenv\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "filetime_0.2.27": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.27\",\"target\":\"cfg(unix)\"},{\"name\":\"libredox\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{}}", @@ -1006,6 +1011,7 @@ "glob_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"}],\"features\":{}}", "globset_0.4.18": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}", "gobject-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-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\":{\"v2_58\":[],\"v2_62\":[\"v2_58\"],\"v2_66\":[\"v2_62\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_74\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}", + "group_0.13.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ff\",\"req\":\"^0.13\"},{\"name\":\"memuse\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"name\":\"rand_xorshift\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.2.1\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"tests\":[\"alloc\",\"rand\",\"rand_xorshift\"],\"wnaf-memuse\":[\"alloc\",\"memuse\"]}}", "gzip-header_1.0.0": "{\"dependencies\":[{\"name\":\"crc32fast\",\"req\":\"^1.2.1\"}],\"features\":{}}", "h2_0.4.13": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "h2_0.4.6": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.26\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", @@ -1249,6 +1255,7 @@ "os_info_3.14.0": "{\"dependencies\":[{\"name\":\"android_system_properties\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"feature\"],\"name\":\"nix\",\"req\":\"^0.30\",\"target\":\"cfg(any(target_os = \\\"aix\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"illumos\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"cygwin\\\"))\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSData\",\"NSError\",\"NSEnumerator\",\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"objc2-ui-kit\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_System_Registry\",\"Win32_System_SystemInformation\",\"Win32_System_SystemServices\",\"Win32_System_Threading\",\"Win32_UI_WindowsAndMessaging\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"serde\"]}}", "os_pipe_1.2.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.62\",\"target\":\"cfg(not(windows))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Pipes\",\"Win32_Security\"],\"name\":\"windows-sys\",\"req\":\">=0.28, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"io_safety\":[]}}", "owo-colors_4.3.0": "{\"dependencies\":[{\"name\":\"supports-color\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"supports-color-2\",\"optional\":true,\"package\":\"supports-color\",\"req\":\"^2.0\"}],\"features\":{\"alloc\":[],\"supports-colors\":[\"dep:supports-color-2\",\"supports-color\"]}}", + "p256_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"der\"],\"name\":\"ecdsa-core\",\"optional\":true,\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"ecdsa-core\",\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"hazmat\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.1\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"primeorder\",\"optional\":true,\"req\":\"^0.13\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"primeorder\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"alloc\":[\"ecdsa-core?/alloc\",\"elliptic-curve/alloc\"],\"arithmetic\":[\"dep:primeorder\",\"elliptic-curve/arithmetic\"],\"bits\":[\"arithmetic\",\"elliptic-curve/bits\"],\"default\":[\"arithmetic\",\"ecdsa\",\"pem\",\"std\"],\"digest\":[\"ecdsa-core/digest\",\"ecdsa-core/hazmat\"],\"ecdh\":[\"arithmetic\",\"elliptic-curve/ecdh\"],\"ecdsa\":[\"arithmetic\",\"ecdsa-core/signing\",\"ecdsa-core/verifying\",\"sha256\"],\"expose-field\":[\"arithmetic\"],\"hash2curve\":[\"arithmetic\",\"elliptic-curve/hash2curve\"],\"jwk\":[\"elliptic-curve/jwk\"],\"pem\":[\"elliptic-curve/pem\",\"ecdsa-core/pem\",\"pkcs8\"],\"pkcs8\":[\"ecdsa-core?/pkcs8\",\"elliptic-curve/pkcs8\"],\"serde\":[\"ecdsa-core?/serde\",\"elliptic-curve/serde\",\"primeorder?/serde\",\"serdect\"],\"sha256\":[\"digest\",\"sha2\"],\"std\":[\"alloc\",\"ecdsa-core?/std\",\"elliptic-curve/std\"],\"test-vectors\":[\"dep:hex-literal\"],\"voprf\":[\"elliptic-curve/voprf\",\"sha2\"]}}", "parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}", "parking_lot_0.12.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"lock_api\",\"req\":\"^0.4.14\"},{\"name\":\"parking_lot_core\",\"req\":\"^0.9.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"}],\"features\":{\"arc_lock\":[\"lock_api/arc_lock\"],\"deadlock_detection\":[\"parking_lot_core/deadlock_detection\"],\"default\":[],\"hardware-lock-elision\":[],\"nightly\":[\"parking_lot_core/nightly\",\"lock_api/nightly\"],\"owning_ref\":[\"lock_api/owning_ref\"],\"send_guard\":[],\"serde\":[\"lock_api/serde\"]}}", "parking_lot_core_0.9.12": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.60\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.95\",\"target\":\"cfg(unix)\"},{\"name\":\"petgraph\",\"optional\":true,\"req\":\"^0.6.0\"},{\"name\":\"redox_syscall\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"deadlock_detection\":[\"petgraph\",\"backtrace\"],\"nightly\":[]}}", @@ -1298,6 +1305,7 @@ "predicates_3.1.3": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"name\":\"difflib\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"float-cmp\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"normalize-line-endings\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"predicates-tree\",\"req\":\"^1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"color\":[],\"default\":[\"diff\",\"regex\",\"float-cmp\",\"normalize-line-endings\",\"color\"],\"diff\":[\"dep:difflib\"],\"unstable\":[]}}", "pretty_assertions_1.4.1": "{\"dependencies\":[{\"name\":\"diff\",\"req\":\"^0.1.12\"},{\"name\":\"yansi\",\"req\":\"^1.0.1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", "prettyplease_0.2.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"default_features\":false,\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.105\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"extra-traits\",\"parsing\",\"printing\",\"visit-mut\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.105\"}],\"features\":{\"verbatim\":[\"syn/parsing\"]}}", + "primeorder_0.13.6": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"arithmetic\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.7\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"}],\"features\":{\"alloc\":[\"elliptic-curve/alloc\"],\"dev\":[],\"serde\":[\"elliptic-curve/serde\",\"serdect\"],\"std\":[\"alloc\",\"elliptic-curve/std\"]}}", "proc-macro-crate_3.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.94\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.39\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.99\"},{\"default_features\":false,\"features\":[\"parse\"],\"name\":\"toml_edit\",\"req\":\"^0.23.2\"}],\"features\":{}}", "proc-macro-error-attr2_2.0.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}", "proc-macro-error2_2.0.1": "{\"dependencies\":[{\"name\":\"proc-macro-error-attr2\",\"req\":\"=2.0.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.99\"}],\"features\":{\"default\":[\"syn-error\"],\"nightly\":[],\"syn-error\":[\"dep:syn\"]}}", @@ -1379,6 +1387,7 @@ "reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}", "resb_0.1.1": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"nom\",\"optional\":true,\"req\":\"^7.0.0\"},{\"default_features\":false,\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.220\"}],\"features\":{\"default\":[],\"logging\":[\"dep:log\"],\"serialize\":[\"std\"],\"std\":[],\"text\":[\"dep:indexmap\",\"dep:nom\",\"std\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", + "rfc6979_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "rmcp_0.15.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}", @@ -1421,6 +1430,7 @@ "scratch_1.0.9": "{\"dependencies\":[],\"features\":{}}", "scrypt_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"password-hash\",\"req\":\"^0.5\"},{\"name\":\"pbkdf2\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"salsa20\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\",\"std\"],\"simple\":[\"password-hash\"],\"std\":[\"password-hash/std\"]}}", "sdd_3.0.10": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{}}", + "sec1_0.7.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"base16ct\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"oid\"],\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"der?/alloc\",\"pkcs8?/alloc\",\"zeroize?/alloc\"],\"default\":[\"der\",\"point\"],\"der\":[\"dep:der\",\"zeroize\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8/pem\"],\"point\":[\"dep:base16ct\",\"dep:generic-array\"],\"serde\":[\"dep:serdect\"],\"std\":[\"alloc\",\"der?/std\"],\"zeroize\":[\"dep:zeroize\",\"der?/zeroize\"]}}", "seccompiler_0.5.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.153\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.27\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.9\"}],\"features\":{\"json\":[\"serde\",\"serde_json\"]}}", "secrecy_0.10.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"zeroize\",\"req\":\"^1.6\"}],\"features\":{}}", "secret-service_4.0.0": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"block-padding\",\"alloc\"],\"name\":\"cbc\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"generic-array\",\"req\":\"^0.14\"},{\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"num\",\"req\":\"^0.4.0\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.40\"},{\"name\":\"rand\",\"req\":\"^0.8.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.103\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"test-with\",\"req\":\"^0.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"zbus\",\"req\":\"^4\"}],\"features\":{\"crypto-openssl\":[\"dep:openssl\"],\"crypto-rust\":[\"dep:aes\",\"dep:cbc\",\"dep:sha2\",\"dep:hkdf\"],\"rt-async-io-crypto-openssl\":[\"zbus/async-io\",\"crypto-openssl\"],\"rt-async-io-crypto-rust\":[\"zbus/async-io\",\"crypto-rust\"],\"rt-tokio-crypto-openssl\":[\"zbus/tokio\",\"crypto-openssl\"],\"rt-tokio-crypto-rust\":[\"zbus/tokio\",\"crypto-rust\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index de1db07093..25c12ac154 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -842,6 +842,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -2117,6 +2123,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "codex-device-key" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "p256", + "pretty_assertions", + "rand 0.9.3", + "serde", + "serde_json", + "thiserror 2.0.18", + "url", +] + [[package]] name = "codex-exec" version = "0.0.0" @@ -3713,6 +3733,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -4417,6 +4449,20 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -4450,6 +4496,26 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -4703,6 +4769,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -6097,6 +6173,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gzip-header" version = "1.0.0" @@ -8573,6 +8660,18 @@ dependencies = [ "supports-color 3.0.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -9002,6 +9101,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -9940,6 +10048,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -10399,6 +10517,20 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "seccompiler" version = "0.5.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 75e54abfba..2364120511 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -24,6 +24,7 @@ members = [ "collaboration-mode-templates", "connectors", "config", + "device-key", "shell-command", "shell-escalation", "skills", @@ -283,6 +284,7 @@ os_info = "3.12.0" owo-colors = "4.3.0" path-absolutize = "3.1.1" pathdiff = "0.2" +p256 = "0.13.2" portable-pty = "0.9.0" predicates = "3" pretty_assertions = "1.4.1" diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 396b7701be..0c841693d3 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -314,7 +314,11 @@ pub fn build_exec_request( windows_sandbox_level, windows_sandbox_private_desktop, }) - .map(|request| ExecRequest::from_sandbox_exec_request(request, options)) + .map(|request| { + let windows_sandbox_policy_cwd = AbsolutePathBuf::try_from(sandbox_cwd.to_path_buf()) + .unwrap_or_else(|_| request.cwd.clone()); + ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_policy_cwd) + }) .map_err(CodexErr::from)?; let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend( exec_req.windows_sandbox_level, @@ -357,6 +361,7 @@ pub(crate) async fn execute_exec_request( expiration, capture_policy, sandbox, + windows_sandbox_policy_cwd: _, windows_sandbox_level, windows_sandbox_private_desktop, sandbox_policy, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index d4e83a6637..09e31274e7 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -49,6 +49,7 @@ pub struct ExecRequest { pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub sandbox: SandboxType, + pub windows_sandbox_policy_cwd: AbsolutePathBuf, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub sandbox_policy: SandboxPolicy, @@ -75,6 +76,7 @@ impl ExecRequest { network_sandbox_policy: NetworkSandboxPolicy, arg0: Option, ) -> Self { + let windows_sandbox_policy_cwd = cwd.clone(); Self { command, cwd, @@ -84,6 +86,7 @@ impl ExecRequest { expiration, capture_policy, sandbox, + windows_sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop, sandbox_policy, @@ -97,6 +100,7 @@ impl ExecRequest { pub(crate) fn from_sandbox_exec_request( request: SandboxExecRequest, options: ExecOptions, + windows_sandbox_policy_cwd: AbsolutePathBuf, ) -> Self { let SandboxExecRequest { command, @@ -134,6 +138,7 @@ impl ExecRequest { expiration, capture_policy, sandbox, + windows_sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop, sandbox_policy, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 7942d8d28a..5587dd46f8 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -171,6 +171,7 @@ pub(crate) async fn execute_user_shell_command( expiration: USER_SHELL_TIMEOUT_MS.into(), capture_policy: ExecCapturePolicy::ShellTool, sandbox: SandboxType::None, + windows_sandbox_policy_cwd: cwd.clone(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 0217cab33a..78072f19de 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -193,8 +193,7 @@ pub(super) async fn apply_granted_turn_permissions( ); let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) { (Some(effective_permissions), Some(granted_permissions)) => { - intersect_permission_profiles(effective_permissions.clone(), granted_permissions, cwd) - == *effective_permissions + permissions_are_preapproved(effective_permissions, granted_permissions, cwd) } _ => false, }; @@ -213,17 +212,37 @@ pub(super) async fn apply_granted_turn_permissions( } } +fn permissions_are_preapproved( + effective_permissions: &PermissionProfile, + granted_permissions: PermissionProfile, + cwd: &Path, +) -> bool { + let materialized_effective_permissions = intersect_permission_profiles( + effective_permissions.clone(), + effective_permissions.clone(), + cwd, + ); + intersect_permission_profiles(effective_permissions.clone(), granted_permissions, cwd) + == materialized_effective_permissions +} + #[cfg(test)] mod tests { use super::EffectiveAdditionalPermissions; use super::implicit_granted_permissions; use super::normalize_and_validate_additional_permissions; + use super::permissions_are_preapproved; use crate::sandboxing::SandboxPermissions; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::FileSystemAccessMode; + use codex_protocol::permissions::FileSystemPath; + use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; + use codex_sandboxing::policy_transforms::intersect_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; @@ -326,4 +345,40 @@ mod tests { assert_eq!(implicit_permissions, None); } + + #[test] + fn relative_deny_glob_grants_remain_preapproved_after_materialization() { + let cwd = tempdir().expect("tempdir"); + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }, + ], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + let stored_grant = intersect_permission_profiles( + requested_permissions.clone(), + requested_permissions.clone(), + cwd.path(), + ); + + assert!(permissions_are_preapproved( + &requested_permissions, + stored_grant, + cwd.path(), + )); + } } diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index d38ecae44c..c91d0fec51 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1091,7 +1091,11 @@ impl JsReplManager { .windows_sandbox_private_desktop, }) .map(|request| { - crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options) + crate::sandboxing::ExecRequest::from_sandbox_exec_request( + request, + options, + turn.cwd.clone(), + ) }) .map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?; diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 00363c38c9..c158486d54 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -134,6 +134,7 @@ pub(super) async fn try_run_zsh_fork( expiration: _sandbox_expiration, capture_policy: _capture_policy, sandbox, + windows_sandbox_policy_cwd: sandbox_policy_cwd, windows_sandbox_level, windows_sandbox_private_desktop: _windows_sandbox_private_desktop, sandbox_policy, @@ -161,7 +162,7 @@ pub(super) async fn try_run_zsh_fork( network: sandbox_network, windows_sandbox_level, arg0, - sandbox_policy_cwd: ctx.turn.cwd.clone(), + sandbox_policy_cwd, codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(), use_legacy_landlock: ctx.turn.features.use_legacy_landlock(), }; @@ -789,6 +790,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { expiration: ExecExpiration::Cancellation(cancel_rx), capture_policy: ExecCapturePolicy::ShellTool, sandbox: self.sandbox, + windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(), windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, sandbox_policy: self.sandbox_policy.clone(), @@ -932,8 +934,11 @@ impl CoreShellCommandExecutor { windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, })?; - let mut exec_request = - crate::sandboxing::ExecRequest::from_sandbox_exec_request(exec_request, options); + let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request( + exec_request, + options, + self.sandbox_policy_cwd.clone(), + ); if let Some(network) = exec_request.network.as_ref() { network.apply_to_env(&mut exec_request.env); } diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 87bef4617c..a40fa0fed0 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -376,7 +376,16 @@ impl<'a> SandboxAttempt<'a> { windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, }) .map(|request| { - crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options) + let windows_sandbox_policy_cwd = + codex_utils_absolute_path::AbsolutePathBuf::try_from( + self.sandbox_cwd.to_path_buf(), + ) + .unwrap_or_else(|_| request.cwd.clone()); + crate::sandboxing::ExecRequest::from_sandbox_exec_request( + request, + options, + windows_sandbox_policy_cwd, + ) }) } } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 5181359698..ef1f3bd81c 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -659,6 +659,60 @@ impl UnifiedExecProcessManager { environment: &codex_exec_server::Environment, ) -> Result { let inherited_fds = spawn_lifecycle.inherited_fds(); + + #[cfg(target_os = "windows")] + if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken { + let policy_json = serde_json::to_string(&request.sandbox_policy).map_err(|err| { + UnifiedExecError::create_process(format!( + "failed to serialize Windows sandbox policy: {err}" + )) + })?; + let codex_home = crate::config::find_codex_home().map_err(|err| { + UnifiedExecError::create_process(format!( + "windows sandbox: failed to resolve codex_home: {err}" + )) + })?; + let spawned = match request.windows_sandbox_level { + codex_protocol::config_types::WindowsSandboxLevel::Elevated => { + codex_windows_sandbox::spawn_windows_sandbox_session_elevated( + policy_json.as_str(), + request.windows_sandbox_policy_cwd.as_path(), + codex_home.as_ref(), + request.command.clone(), + request.cwd.as_path(), + request.env.clone(), + None, + tty, + tty, + request.windows_sandbox_private_desktop, + ) + .await + } + codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken + | codex_protocol::config_types::WindowsSandboxLevel::Disabled => { + codex_windows_sandbox::spawn_windows_sandbox_session_legacy( + policy_json.as_str(), + request.windows_sandbox_policy_cwd.as_path(), + codex_home.as_ref(), + request.command.clone(), + request.cwd.as_path(), + request.env.clone(), + None, + tty, + tty, + request.windows_sandbox_private_desktop, + ) + .await + } + }; + spawn_lifecycle.after_spawn(); + return UnifiedExecProcess::from_spawned( + spawned.map_err(|err| UnifiedExecError::create_process(err.to_string()))?, + request.sandbox, + spawn_lifecycle, + ) + .await; + } if environment.is_remote() { if !inherited_fds.is_empty() { return Err(UnifiedExecError::create_process( diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 2c829cdd0e..fac05f2b4b 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -67,12 +67,13 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() { #[test] fn exec_server_params_use_env_policy_overlay_contract() { + let cwd: codex_utils_absolute_path::AbsolutePathBuf = std::env::current_dir() + .expect("current dir") + .try_into() + .expect("absolute path"); let request = ExecRequest { command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()], - cwd: std::env::current_dir() - .expect("current dir") - .try_into() - .expect("absolute path"), + cwd: cwd.clone(), env: HashMap::from([ ("HOME".to_string(), "/client-home".to_string()), ("PATH".to_string(), "/sandbox-path".to_string()), @@ -95,6 +96,7 @@ fn exec_server_params_use_env_policy_overlay_contract() { expiration: crate::exec::ExecExpiration::DefaultTimeout, capture_policy: crate::exec::ExecCapturePolicy::ShellTool, sandbox: codex_sandboxing::SandboxType::None, + windows_sandbox_policy_cwd: cwd, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, diff --git a/codex-rs/device-key/BUILD.bazel b/codex-rs/device-key/BUILD.bazel new file mode 100644 index 0000000000..4ad47f84a0 --- /dev/null +++ b/codex-rs/device-key/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "device-key", + crate_name = "codex_device_key", +) diff --git a/codex-rs/device-key/Cargo.toml b/codex-rs/device-key/Cargo.toml new file mode 100644 index 0000000000..f61a886e01 --- /dev/null +++ b/codex-rs/device-key/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "codex-device-key" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +base64 = { workspace = true } +p256 = { workspace = true, features = ["ecdsa", "pkcs8"] } +rand = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/device-key/src/lib.rs b/codex-rs/device-key/src/lib.rs new file mode 100644 index 0000000000..61d34e034e --- /dev/null +++ b/codex-rs/device-key/src/lib.rs @@ -0,0 +1,1374 @@ +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use p256::pkcs8::EncodePublicKey; +use rand::random; +use serde::Deserialize; +use serde::Serialize; +use std::fmt; +use std::fmt::Debug; +use std::sync::Arc; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use thiserror::Error; +use url::Host; +use url::Url; + +mod platform; + +const SIGNING_DOMAIN: &str = "codex-device-key-sign-payload/v1"; +const DEVICE_KEY_ID_RANDOM_BYTES: usize = 32; +const DEVICE_KEY_ID_ENCODED_BYTES: usize = 43; +const DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX: &str = "dk_hse_"; +const DEVICE_KEY_ID_HARDWARE_TPM_PREFIX: &str = "dk_tpm_"; +const DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX: &str = "dk_osn_"; +const DEVICE_KEY_ID_PREFIX_LEN: usize = DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX.len(); +const DEVICE_KEY_ID_LEN: usize = DEVICE_KEY_ID_PREFIX_LEN + DEVICE_KEY_ID_ENCODED_BYTES; +const INVALID_DEVICE_KEY_ID_MESSAGE: &str = + "keyId must be dk_hse_, dk_tpm_, or dk_osn_ followed by unpadded base64url-encoded 32 bytes"; +const REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE: &str = "remote_control_controller_websocket"; +const MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS: i64 = 15 * 60; +const REMOTE_CONTROL_CLIENT_CONNECTION_PATHS: &[&str] = &[ + "/api/codex/remote/control/client", + "/wham/remote/control/client", +]; +const REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS: &[&str] = &[ + "/api/codex/remote/control/client/enroll", + "/wham/remote/control/client/enroll", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DeviceKeyAlgorithm { + EcdsaP256Sha256, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DeviceKeyProtectionClass { + HardwareSecureEnclave, + HardwareTpm, + OsProtectedNonextractable, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceKeyProtectionPolicy { + HardwareOnly, + AllowOsProtectedNonextractable, +} + +impl DeviceKeyProtectionPolicy { + fn allows(self, protection_class: DeviceKeyProtectionClass) -> bool { + match self { + Self::HardwareOnly => !protection_class.is_degraded(), + Self::AllowOsProtectedNonextractable => matches!( + protection_class, + DeviceKeyProtectionClass::HardwareSecureEnclave + | DeviceKeyProtectionClass::HardwareTpm + | DeviceKeyProtectionClass::OsProtectedNonextractable + ), + } + } +} + +impl DeviceKeyProtectionClass { + pub fn is_degraded(self) -> bool { + match self { + Self::HardwareSecureEnclave | Self::HardwareTpm => false, + Self::OsProtectedNonextractable => true, + } + } + + fn key_id_prefix(self) -> &'static str { + match self { + Self::HardwareSecureEnclave => DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX, + Self::HardwareTpm => DEVICE_KEY_ID_HARDWARE_TPM_PREFIX, + Self::OsProtectedNonextractable => DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX, + } + } +} + +impl fmt::Display for DeviceKeyProtectionClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HardwareSecureEnclave => f.write_str("hardware_secure_enclave"), + Self::HardwareTpm => f.write_str("hardware_tpm"), + Self::OsProtectedNonextractable => f.write_str("os_protected_nonextractable"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyCreateRequest { + pub protection_policy: DeviceKeyProtectionPolicy, + pub binding: DeviceKeyBinding, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyGetPublicRequest { + pub key_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeySignRequest { + pub key_id: String, + pub payload: DeviceKeySignPayload, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyBinding { + pub account_user_id: String, + pub client_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyInfo { + pub key_id: String, + pub public_key_spki_der: Vec, + pub algorithm: DeviceKeyAlgorithm, + pub protection_class: DeviceKeyProtectionClass, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeySignature { + pub signature_der: Vec, + /// Exact payload bytes covered by `signature_der`. + pub signed_payload: Vec, + pub algorithm: DeviceKeyAlgorithm, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ProviderSignature { + signature_der: Vec, + algorithm: DeviceKeyAlgorithm, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum DeviceKeySignPayload { + RemoteControlClientConnection(RemoteControlClientConnectionSignPayload), + RemoteControlClientEnrollment(RemoteControlClientEnrollmentSignPayload), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RemoteControlClientConnectionAudience { + RemoteControlClientWebsocket, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteControlClientConnectionSignPayload { + pub nonce: String, + pub audience: RemoteControlClientConnectionAudience, + pub session_id: String, + pub target_origin: String, + pub target_path: String, + pub account_user_id: String, + pub client_id: String, + pub token_sha256_base64url: String, + pub token_expires_at: i64, + pub scopes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RemoteControlClientEnrollmentAudience { + RemoteControlClientEnrollment, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteControlClientEnrollmentSignPayload { + pub nonce: String, + pub audience: RemoteControlClientEnrollmentAudience, + pub challenge_id: String, + pub target_origin: String, + pub target_path: String, + pub account_user_id: String, + pub client_id: String, + pub device_identity_sha256_base64url: String, + pub challenge_expires_at: i64, +} + +#[derive(Debug, Error)] +pub enum DeviceKeyError { + #[error( + "hardware-backed device keys are not available; set protectionPolicy to allow_os_protected_nonextractable to allow key protection class {available}" + )] + DegradedProtectionNotAllowed { available: DeviceKeyProtectionClass }, + #[error("hardware-backed device keys are not available on this platform")] + HardwareBackedKeysUnavailable, + #[error("device key not found")] + KeyNotFound, + #[error("invalid device key payload: {0}")] + InvalidPayload(&'static str), + #[error("device key platform error: {0}")] + Platform(String), + #[error("device key cryptography error: {0}")] + Crypto(String), +} + +#[derive(Debug, Clone)] +pub struct DeviceKeyStore { + provider: Arc, +} + +impl Default for DeviceKeyStore { + fn default() -> Self { + Self { + provider: platform::default_provider(), + } + } +} + +impl DeviceKeyStore { + pub fn create(&self, request: DeviceKeyCreateRequest) -> Result { + let key_id_random = random_key_id_random(); + validate_binding(&request.binding.account_user_id, &request.binding.client_id)?; + self.provider.create(ProviderCreateRequest { + key_id_random: &key_id_random, + protection_policy: request.protection_policy, + binding: &request.binding, + }) + } + + pub fn get_public( + &self, + request: DeviceKeyGetPublicRequest, + ) -> Result { + let protection_class = validate_key_id(&request.key_id)?; + self.provider.get_public(&request.key_id, protection_class) + } + + pub fn sign( + &self, + request: DeviceKeySignRequest, + ) -> Result { + let protection_class = validate_key_id(&request.key_id)?; + validate_payload(&request.payload)?; + let binding = self.provider.binding(&request.key_id, protection_class)?; + validate_payload_binding(&request.payload, &binding)?; + let signed_payload = device_key_signing_payload_bytes(&request.payload)?; + let signature = self + .provider + .sign(&request.key_id, protection_class, &signed_payload)?; + Ok(DeviceKeySignature { + signature_der: signature.signature_der, + signed_payload, + algorithm: signature.algorithm, + }) + } + + #[cfg(test)] + fn with_provider(provider: Arc) -> Self { + Self { provider } + } +} + +#[derive(Debug)] +struct ProviderCreateRequest<'a> { + key_id_random: &'a str, + protection_policy: DeviceKeyProtectionPolicy, + binding: &'a DeviceKeyBinding, +} + +impl ProviderCreateRequest<'_> { + fn key_id_for(&self, protection_class: DeviceKeyProtectionClass) -> String { + key_id_for_protection_class(protection_class, self.key_id_random) + } +} + +/// Owns platform-specific non-exportable key operations for device signing. +/// +/// Implementations must never expose a generic arbitrary-byte signing API outside this crate. The +/// crate validates and serializes accepted structured payloads before calling `sign`. +trait DeviceKeyProvider: Debug + Send + Sync { + fn create(&self, request: ProviderCreateRequest<'_>) -> Result; + fn get_public( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result; + fn binding( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result; + fn sign( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + payload: &[u8], + ) -> Result; +} + +fn random_key_id_random() -> String { + URL_SAFE_NO_PAD.encode(random::<[u8; DEVICE_KEY_ID_RANDOM_BYTES]>()) +} + +fn key_id_for_protection_class( + protection_class: DeviceKeyProtectionClass, + encoded_random: &str, +) -> String { + format!("{}{encoded_random}", protection_class.key_id_prefix()) +} + +/// Validates the account/client binding stored with a key or embedded in an accepted payload. +/// +/// Providers treat the binding as metadata, so this crate keeps empty values from entering the +/// store and later matching every other empty value by accident. +fn validate_binding(account_user_id: &str, client_id: &str) -> Result<(), DeviceKeyError> { + if account_user_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "accountUserId must not be empty", + )); + } + if client_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload("clientId must not be empty")); + } + Ok(()) +} + +/// Keeps all externally supplied key IDs inside the random `dk_*_` namespaces created by this crate. +/// +/// Platform providers use the key ID in OS-specific labels, tags, and metadata paths. Requiring the +/// exact generated shape avoids path or tag surprises and makes the namespace auditable. +fn validate_key_id(key_id: &str) -> Result { + let (protection_class, encoded_key) = parse_key_id(key_id).ok_or( + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE), + )?; + if key_id.len() != DEVICE_KEY_ID_LEN { + return Err(DeviceKeyError::InvalidPayload( + INVALID_DEVICE_KEY_ID_MESSAGE, + )); + } + if !URL_SAFE_NO_PAD + .decode(encoded_key) + .is_ok_and(|decoded| decoded.len() == DEVICE_KEY_ID_RANDOM_BYTES) + { + return Err(DeviceKeyError::InvalidPayload( + INVALID_DEVICE_KEY_ID_MESSAGE, + )); + } + Ok(protection_class) +} + +fn parse_key_id(key_id: &str) -> Option<(DeviceKeyProtectionClass, &str)> { + for protection_class in [ + DeviceKeyProtectionClass::HardwareSecureEnclave, + DeviceKeyProtectionClass::HardwareTpm, + DeviceKeyProtectionClass::OsProtectedNonextractable, + ] { + if let Some(encoded_key) = key_id.strip_prefix(protection_class.key_id_prefix()) { + return Some((protection_class, encoded_key)); + } + } + None +} + +/// Confirms the signed payload is for the same account/client binding as the selected device key. +/// +/// The provider can prove continuity of the key material, but app-server authorization depends on +/// binding that key to the same account and client identity used by the remote-control flow. +fn validate_payload_binding( + payload: &DeviceKeySignPayload, + binding: &DeviceKeyBinding, +) -> Result<(), DeviceKeyError> { + let (account_user_id, client_id) = match payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + (&payload.account_user_id, &payload.client_id) + } + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + (&payload.account_user_id, &payload.client_id) + } + }; + if account_user_id != &binding.account_user_id || client_id != &binding.client_id { + return Err(DeviceKeyError::InvalidPayload( + "payload accountUserId/clientId does not match device key binding", + )); + } + Ok(()) +} + +/// Dispatches validation by accepted payload shape before any provider sees bytes to sign. +/// +/// The enum is intentionally narrow so adding another signing use case requires defining and +/// validating a new structured payload variant here. +fn validate_payload(payload: &DeviceKeySignPayload) -> Result<(), DeviceKeyError> { + match payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + validate_remote_control_client_connection_payload(payload) + } + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + validate_remote_control_client_enrollment_payload(payload) + } + } +} + +/// Validates payloads used to prove device-key ownership while opening `/client`. +/// +/// This shape is scoped to a single controller websocket connection and is only allowed to target +/// the non-enrollment remote-control client endpoints. +fn validate_remote_control_client_connection_payload( + payload: &RemoteControlClientConnectionSignPayload, +) -> Result<(), DeviceKeyError> { + validate_nonce(&payload.nonce)?; + validate_remote_control_target( + &payload.target_origin, + &payload.target_path, + REMOTE_CONTROL_CLIENT_CONNECTION_PATHS, + )?; + if payload.session_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "sessionId must not be empty", + )); + } + validate_binding(&payload.account_user_id, &payload.client_id)?; + if !is_base64url_sha256(&payload.token_sha256_base64url) { + return Err(DeviceKeyError::InvalidPayload( + "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url", + )); + } + if payload.scopes != [REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE] { + return Err(DeviceKeyError::InvalidPayload( + "scopes must contain exactly remote_control_controller_websocket", + )); + } + validate_remote_control_expiry(payload.token_expires_at, "remote-control token")?; + Ok(()) +} + +/// Validates payloads used during device-key enrollment. +/// +/// Enrollment has a distinct payload shape and challenge identifier, so it also carries a distinct +/// endpoint allowlist from connection proofs. +fn validate_remote_control_client_enrollment_payload( + payload: &RemoteControlClientEnrollmentSignPayload, +) -> Result<(), DeviceKeyError> { + validate_nonce(&payload.nonce)?; + if payload.challenge_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "challengeId must not be empty", + )); + } + validate_remote_control_target( + &payload.target_origin, + &payload.target_path, + REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS, + )?; + validate_binding(&payload.account_user_id, &payload.client_id)?; + if !is_base64url_sha256(&payload.device_identity_sha256_base64url) { + return Err(DeviceKeyError::InvalidPayload( + "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url", + )); + } + validate_remote_control_expiry(payload.challenge_expires_at, "enrollment challenge")?; + Ok(()) +} + +/// Requires a fresh server-issued challenge with enough entropy to prevent replay guessing. +fn validate_nonce(nonce: &str) -> Result<(), DeviceKeyError> { + if !URL_SAFE_NO_PAD + .decode(nonce) + .is_ok_and(|decoded| decoded.len() >= 32) + { + return Err(DeviceKeyError::InvalidPayload( + "nonce must be at least 32 random bytes encoded as unpadded base64url", + )); + } + Ok(()) +} + +/// Validates the remote backend origin and the endpoint set for the specific signed payload shape. +/// +/// Keeping the path allowlist as an argument makes it hard to accidentally let enrollment payloads +/// sign connection endpoints, or connection payloads sign enrollment endpoints. +fn validate_remote_control_target( + target_origin: &str, + target_path: &str, + allowed_target_paths: &[&str], +) -> Result<(), DeviceKeyError> { + if !is_allowed_remote_control_origin(target_origin) { + return Err(DeviceKeyError::InvalidPayload( + "targetOrigin must be an allowed remote-control backend origin", + )); + } + if !allowed_target_paths.contains(&target_path) { + return Err(DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint", + )); + } + Ok(()) +} + +/// Mirrors the remote-control transport allowlist for origins that may receive signed proofs. +fn is_allowed_remote_control_origin(target_origin: &str) -> bool { + let Ok(url) = Url::parse(target_origin) else { + return false; + }; + if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { + return false; + } + let host = url.host(); + match url.scheme() { + "https" if is_localhost(&host) || is_allowed_chatgpt_host(&host) => true, + "http" if is_localhost(&host) => true, + _ => false, + } +} + +/// Accepts first-party chatgpt.com hosts and staging equivalents, including subdomains. +fn is_allowed_chatgpt_host(host: &Option>) -> bool { + let Some(Host::Domain(host)) = *host else { + return false; + }; + host == "chatgpt.com" + || host == "chatgpt-staging.com" + || host.ends_with(".chatgpt.com") + || host.ends_with(".chatgpt-staging.com") +} + +/// Allows local development endpoints without opening access to arbitrary private-network hosts. +fn is_localhost(host: &Option>) -> bool { + match host { + Some(Host::Domain("localhost")) => true, + Some(Host::Ipv4(ip)) => ip.is_loopback(), + Some(Host::Ipv6(ip)) => ip.is_loopback(), + _ => false, + } +} + +/// Bounds remote-control proofs to the connection or enrollment attempt that requested them. +fn validate_remote_control_expiry( + expires_at: i64, + label: &'static str, +) -> Result<(), DeviceKeyError> { + let now = current_unix_seconds()?; + if expires_at <= now { + return Err(DeviceKeyError::InvalidPayload(match label { + "enrollment challenge" => "enrollment challenge is expired", + _ => "remote-control token is expired", + })); + } + if expires_at > now + MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS { + return Err(DeviceKeyError::InvalidPayload(match label { + "enrollment challenge" => "enrollment challenge expires too far in the future", + _ => "remote-control token expires too far in the future", + })); + } + Ok(()) +} + +/// Checks the exact digest encoding used in remote-control challenge and token bindings. +fn is_base64url_sha256(value: &str) -> bool { + URL_SAFE_NO_PAD + .decode(value) + .is_ok_and(|digest| digest.len() == 32) +} + +fn current_unix_seconds() -> Result { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| DeviceKeyError::InvalidPayload("system clock is before Unix epoch"))?; + i64::try_from(duration.as_secs()) + .map_err(|_| DeviceKeyError::InvalidPayload("current time does not fit in i64")) +} + +/// Returns the exact bytes that device-key providers sign and verifiers must check. +/// +/// The representation is UTF-8 JSON with an explicit domain separator, sorted object keys, no +/// insignificant whitespace, and the accepted structured payload. Test vectors in this crate +/// intentionally lock the field names and ordering so non-Rust verifiers can reproduce the same +/// bytes. +pub fn device_key_signing_payload_bytes( + payload: &DeviceKeySignPayload, +) -> Result, DeviceKeyError> { + let mut canonical = serde_json::to_value(SignedPayload { + domain: SIGNING_DOMAIN, + payload, + }) + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; + canonical.sort_all_objects(); + serde_json::to_vec(&canonical).map_err(|err| DeviceKeyError::Crypto(err.to_string())) +} + +#[derive(Serialize)] +struct SignedPayload<'a> { + domain: &'static str, + payload: &'a DeviceKeySignPayload, +} + +#[allow(dead_code)] +fn sec1_public_key_to_spki_der(sec1_public_key: &[u8]) -> Result, DeviceKeyError> { + let public_key = p256::PublicKey::from_sec1_bytes(sec1_public_key) + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; + public_key + .to_public_key_der() + .map(|der| der.as_bytes().to_vec()) + .map_err(|err| DeviceKeyError::Crypto(err.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use p256::ecdsa::Signature; + use p256::ecdsa::SigningKey; + use p256::ecdsa::VerifyingKey; + use p256::ecdsa::signature::Signer; + use p256::ecdsa::signature::Verifier; + use p256::elliptic_curve::rand_core::OsRng; + use p256::pkcs8::DecodePublicKey; + use pretty_assertions::assert_eq; + use std::collections::HashMap; + use std::sync::Mutex; + + const TEST_TOKEN_SHA256_BASE64URL: &str = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"; + const TEST_NONCE_BASE64URL: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + + #[derive(Debug)] + struct MemoryProvider { + class: DeviceKeyProtectionClass, + keys: Mutex>, + bindings: Mutex>, + } + + impl MemoryProvider { + fn new(class: DeviceKeyProtectionClass) -> Self { + Self { + class, + keys: Mutex::new(HashMap::new()), + bindings: Mutex::new(HashMap::new()), + } + } + } + + impl DeviceKeyProvider for MemoryProvider { + fn create( + &self, + request: ProviderCreateRequest<'_>, + ) -> Result { + if !request.protection_policy.allows(self.class) { + return Err(DeviceKeyError::DegradedProtectionNotAllowed { + available: self.class, + }); + } + let key_id = request.key_id_for(self.class); + let mut keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys + .entry(key_id.clone()) + .or_insert_with(|| SigningKey::random(&mut OsRng)); + self.bindings + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))? + .insert(key_id.clone(), request.binding.clone()); + memory_key_info(&key_id, signing_key, self.class) + } + + fn get_public( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + let keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; + memory_key_info(key_id, signing_key, self.class) + } + + fn binding( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + self.bindings + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))? + .get(key_id) + .cloned() + .ok_or(DeviceKeyError::KeyNotFound) + } + + fn sign( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + payload: &[u8], + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + let keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; + let signature: Signature = signing_key.sign(payload); + Ok(ProviderSignature { + signature_der: signature.to_der().as_bytes().to_vec(), + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + }) + } + } + + fn memory_key_info( + key_id: &str, + signing_key: &SigningKey, + class: DeviceKeyProtectionClass, + ) -> Result { + let public_key_spki_der = signing_key + .verifying_key() + .to_public_key_der() + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))? + .as_bytes() + .to_vec(); + Ok(DeviceKeyInfo { + key_id: key_id.to_string(), + public_key_spki_der, + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + protection_class: class, + }) + } + + fn store(class: DeviceKeyProtectionClass) -> DeviceKeyStore { + DeviceKeyStore::with_provider(Arc::new(MemoryProvider::new(class))) + } + + fn create_request(protection_policy: DeviceKeyProtectionPolicy) -> DeviceKeyCreateRequest { + DeviceKeyCreateRequest { + protection_policy, + binding: DeviceKeyBinding { + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + }, + } + } + + fn remote_control_client_connection_payload() -> DeviceKeySignPayload { + DeviceKeySignPayload::RemoteControlClientConnection( + RemoteControlClientConnectionSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, + session_id: "wssess_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/api/codex/remote/control/client".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + token_expires_at: current_unix_seconds().expect("time should be valid") + 60, + scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], + }, + ) + } + + fn remote_control_client_enrollment_payload() -> DeviceKeySignPayload { + DeviceKeySignPayload::RemoteControlClientEnrollment( + RemoteControlClientEnrollmentSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, + challenge_id: "rch_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/wham/remote/control/client/enroll".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + challenge_expires_at: current_unix_seconds().expect("time should be valid") + 60, + }, + ) + } + + fn assert_valid_generated_key_id(key_id: &str, expected_class: DeviceKeyProtectionClass) { + assert_eq!(key_id.len(), DEVICE_KEY_ID_LEN); + assert_eq!( + validate_key_id(key_id).expect("generated key id should be valid"), + expected_class + ); + let encoded_key = key_id + .strip_prefix(expected_class.key_id_prefix()) + .expect("generated key id should use protection-class prefix"); + assert_eq!(encoded_key.len(), DEVICE_KEY_ID_ENCODED_BYTES); + assert_eq!( + URL_SAFE_NO_PAD + .decode(encoded_key) + .expect("generated key id should be base64url") + .len(), + DEVICE_KEY_ID_RANDOM_BYTES + ); + } + + #[test] + fn create_requires_explicit_degraded_protection() { + let err = store(DeviceKeyProtectionClass::OsProtectedNonextractable) + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect_err("OS-protected fallback should require opt-in"); + + assert!( + matches!( + err, + DeviceKeyError::DegradedProtectionNotAllowed { + available: DeviceKeyProtectionClass::OsProtectedNonextractable, + } + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn create_allows_os_protected_nonextractable_policy() { + let info = store(DeviceKeyProtectionClass::OsProtectedNonextractable) + .create(create_request( + DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable, + )) + .expect("OS-protected fallback should be allowed by policy"); + + assert_eq!( + info.protection_class, + DeviceKeyProtectionClass::OsProtectedNonextractable + ); + assert_valid_generated_key_id( + &info.key_id, + DeviceKeyProtectionClass::OsProtectedNonextractable, + ); + } + + #[test] + fn create_generates_distinct_key_ids() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let first = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let second = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + + assert_ne!(second.key_id, first.key_id); + assert_valid_generated_key_id(&first.key_id, DeviceKeyProtectionClass::HardwareTpm); + assert_valid_generated_key_id(&second.key_id, DeviceKeyProtectionClass::HardwareTpm); + } + + #[test] + fn key_id_validation_rejects_untrusted_namespaces() { + let valid_suffix = URL_SAFE_NO_PAD.encode([0_u8; DEVICE_KEY_ID_RANDOM_BYTES]); + + for key_id in [ + String::new(), + "dk_".to_string(), + "dk_hse_".to_string(), + format!("bad_{valid_suffix}"), + format!("dk_bad_{valid_suffix}"), + format!( + "{}{}", + DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + format!( + "{}{valid_suffix}A", + DeviceKeyProtectionClass::HardwareTpm.key_id_prefix() + ), + format!( + "{}{}=", + DeviceKeyProtectionClass::OsProtectedNonextractable.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + format!( + "{}{}+", + DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + ] { + let err = validate_key_id(&key_id).expect_err("malformed key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected error for {key_id:?}: {err:?}" + ); + } + } + + #[test] + fn public_operations_reject_malformed_key_id_before_provider_use() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let malformed_key_id = "not-a-device-key".to_string(); + + let err = store + .get_public(DeviceKeyGetPublicRequest { + key_id: malformed_key_id.clone(), + }) + .expect_err("malformed get_public key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected get_public error: {err:?}" + ); + + let err = store + .sign(DeviceKeySignRequest { + key_id: malformed_key_id, + payload: remote_control_client_connection_payload(), + }) + .expect_err("malformed sign key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected sign error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_account_user_id() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.account_user_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty account user id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("accountUserId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_uses_structured_payload() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let payload = remote_control_client_connection_payload(); + let signed_payload = + device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + let signature = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect("sign should succeed"); + assert_eq!(signature.signed_payload, signed_payload); + + let verifying_key = VerifyingKey::from_public_key_der(&info.public_key_spki_der) + .expect("public key should decode"); + let signature = + Signature::from_der(&signature.signature_der).expect("signature should decode"); + verifying_key + .verify(&signed_payload, &signature) + .expect("signature should verify against structured payload"); + } + + #[test] + fn signing_payload_bytes_are_stable() { + let payload = DeviceKeySignPayload::RemoteControlClientConnection( + RemoteControlClientConnectionSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, + session_id: "wssess_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/api/codex/remote/control/client".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + token_expires_at: 1_700_000_000, + scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], + }, + ); + + let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + + assert_eq!( + String::from_utf8(bytes).expect("payload should be utf-8"), + concat!( + "{\"domain\":\"codex-device-key-sign-payload/v1\",", + "\"payload\":{\"accountUserId\":\"account-user-1\",", + "\"audience\":\"remote_control_client_websocket\",", + "\"clientId\":\"cli_123\",", + "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", + "\"scopes\":[\"remote_control_controller_websocket\"],", + "\"sessionId\":\"wssess_123\",", + "\"targetOrigin\":\"https://chatgpt.com\",", + "\"targetPath\":\"/api/codex/remote/control/client\",", + "\"tokenExpiresAt\":1700000000,", + "\"tokenSha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", + "\"type\":\"remoteControlClientConnection\"}}" + ) + ); + } + + #[test] + fn enrollment_signing_payload_bytes_are_stable() { + let payload = DeviceKeySignPayload::RemoteControlClientEnrollment( + RemoteControlClientEnrollmentSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, + challenge_id: "rch_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/wham/remote/control/client/enroll".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + challenge_expires_at: 1_700_000_060, + }, + ); + + let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + + assert_eq!( + String::from_utf8(bytes).expect("payload should be utf-8"), + concat!( + "{\"domain\":\"codex-device-key-sign-payload/v1\",", + "\"payload\":{\"accountUserId\":\"account-user-1\",", + "\"audience\":\"remote_control_client_enrollment\",", + "\"challengeExpiresAt\":1700000060,", + "\"challengeId\":\"rch_123\",", + "\"clientId\":\"cli_123\",", + "\"deviceIdentitySha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", + "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", + "\"targetOrigin\":\"https://chatgpt.com\",", + "\"targetPath\":\"/wham/remote/control/client/enroll\",", + "\"type\":\"remoteControlClientEnrollment\"}}" + ) + ); + } + + #[test] + fn sign_rejects_malformed_token_hash() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.token_sha256_base64url = "not-a-sha256".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("malformed token hash should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_unexpected_scopes() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.scopes = vec!["other_scope".to_string()]; + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("unexpected scope should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "scopes must contain exactly remote_control_controller_websocket" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_malformed_enrollment_identity_hash() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_enrollment_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientEnrollment(enrollment_payload) => { + enrollment_payload.device_identity_sha256_base64url = "not-a-sha256".to_string(); + } + DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("malformed device identity hash should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_target_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.target_origin.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty target origin should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetOrigin must be an allowed remote-control backend origin" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_remote_control_paths_for_other_payload_shapes() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut connection_payload = remote_control_client_connection_payload(); + match &mut connection_payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + payload.target_path = "/api/codex/remote/control/client/enroll".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id.clone(), + payload: connection_payload, + }) + .expect_err("connection payload should reject enrollment path"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint" + ) + ), + "unexpected connection path error: {err:?}" + ); + + let mut enrollment_payload = remote_control_client_enrollment_payload(); + match &mut enrollment_payload { + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + payload.target_path = "/wham/remote/control/client".to_string(); + } + DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload: enrollment_payload, + }) + .expect_err("enrollment payload should reject connection path"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint" + ) + ), + "unexpected enrollment path error: {err:?}" + ); + } + + #[test] + fn remote_control_origin_matches_remote_transport_allowlist() { + for origin in [ + "https://chatgpt.com", + "https://chatgpt-staging.com", + "https://ab.chatgpt.com", + "https://ab.chatgpt-staging.com", + "http://localhost:8080", + "https://localhost:8443", + "http://127.0.0.1:8080", + "http://[::1]:8080", + ] { + assert!( + is_allowed_remote_control_origin(origin), + "expected allowed origin: {origin}" + ); + } + + for origin in [ + "http://chatgpt.com", + "https://chat.openai.com", + "https://api.openai.com", + "https://chatgpt.com.evil.com", + "https://evilchatgpt.com", + "https://foo.localhost", + "https://localhost.evil.com", + "https://192.168.1.2", + "https://chatgpt.com/backend-api", + "https://chatgpt.com?query=1", + ] { + assert!( + !is_allowed_remote_control_origin(origin), + "expected rejected origin: {origin}" + ); + } + } + + #[test] + fn sign_rejects_empty_session_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.session_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty session id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("sessionId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_client_id() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.client_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty client id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("clientId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_mismatched_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.account_user_id = "other-account-user".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("mismatched binding should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "payload accountUserId/clientId does not match device key binding" + ) + ), + "unexpected error: {err:?}" + ); + } +} diff --git a/codex-rs/device-key/src/platform.rs b/codex-rs/device-key/src/platform.rs new file mode 100644 index 0000000000..3dbcb168e7 --- /dev/null +++ b/codex-rs/device-key/src/platform.rs @@ -0,0 +1,51 @@ +use crate::DeviceKeyBinding; +use crate::DeviceKeyError; +use crate::DeviceKeyInfo; +use crate::DeviceKeyProtectionClass; +use crate::DeviceKeyProvider; +use crate::ProviderCreateRequest; +use crate::ProviderSignature; +use std::sync::Arc; + +pub(crate) fn default_provider() -> Arc { + Arc::new(UnsupportedDeviceKeyProvider) +} + +#[derive(Debug)] +pub(crate) struct UnsupportedDeviceKeyProvider; + +impl DeviceKeyProvider for UnsupportedDeviceKeyProvider { + fn create(&self, request: ProviderCreateRequest<'_>) -> Result { + let _ = request.key_id_for(DeviceKeyProtectionClass::HardwareTpm); + let _ = request + .protection_policy + .allows(DeviceKeyProtectionClass::HardwareTpm); + let _ = request.binding; + Err(DeviceKeyError::HardwareBackedKeysUnavailable) + } + + fn get_public( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } + + fn binding( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } + + fn sign( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + _payload: &[u8], + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } +} diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index a0c93d9122..f7488647da 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -636,7 +636,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { }; let granted = PermissionProfile { file_system: Some(FileSystemPermissions { - entries: vec![root_write.clone(), deny_env_files.clone()], + entries: vec![root_write.clone(), deny_env_files], glob_scan_max_depth: std::num::NonZeroUsize::new(4), }), ..Default::default() @@ -691,7 +691,7 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { }; let granted = PermissionProfile { file_system: Some(FileSystemPermissions { - entries: vec![root_write.clone(), deny_env_files.clone()], + entries: vec![root_write.clone(), deny_env_files], glob_scan_max_depth: None, }), ..Default::default() diff --git a/codex-rs/utils/pty/src/lib.rs b/codex-rs/utils/pty/src/lib.rs index 87898ddf71..f524156651 100644 --- a/codex-rs/utils/pty/src/lib.rs +++ b/codex-rs/utils/pty/src/lib.rs @@ -13,6 +13,8 @@ pub const DEFAULT_OUTPUT_BYTES_CAP: usize = 1024 * 1024; pub use pipe::spawn_process as spawn_pipe_process; /// Spawn a non-interactive process using regular pipes, but close stdin immediately. pub use pipe::spawn_process_no_stdin as spawn_pipe_process_no_stdin; +/// Driver-backed process adapter used by integrations with their own process transport. +pub use process::ProcessDriver; /// Handle for interacting with a spawned process (PTY or pipe). pub use process::ProcessHandle; /// Bundle of process handles plus split output and exit receivers returned by spawn helpers. @@ -21,6 +23,8 @@ pub use process::SpawnedProcess; pub use process::TerminalSize; /// Combine stdout/stderr receivers into a single broadcast receiver. pub use process::combine_output_receivers; +/// Adapt an externally-driven process into the standard spawned-process handle. +pub use process::spawn_from_driver; /// Backwards-compatible alias for ProcessHandle. pub type ExecCommandSession = ProcessHandle; /// Backwards-compatible alias for SpawnedProcess. diff --git a/codex-rs/utils/pty/src/pipe.rs b/codex-rs/utils/pty/src/pipe.rs index 4962a22863..541a2ecf2f 100644 --- a/codex-rs/utils/pty/src/pipe.rs +++ b/codex-rs/utils/pty/src/pipe.rs @@ -234,6 +234,7 @@ async fn spawn_process_with_stdin_mode( exit_status, exit_code, /*pty_handles*/ None, + /*resizer*/ None, ); Ok(SpawnedProcess { diff --git a/codex-rs/utils/pty/src/process.rs b/codex-rs/utils/pty/src/process.rs index 8459f2b68f..a171cdd7af 100644 --- a/codex-rs/utils/pty/src/process.rs +++ b/codex-rs/utils/pty/src/process.rs @@ -13,6 +13,7 @@ use portable_pty::SlavePty; use tokio::sync::broadcast; use tokio::sync::mpsc; use tokio::sync::oneshot; +use tokio::sync::watch; use tokio::task::AbortHandle; use tokio::task::JoinHandle; @@ -69,6 +70,10 @@ impl fmt::Debug for PtyHandles { } } +/// Callback used by driver-backed sessions to resize a PTY-like backend when +/// there is no local `PtyHandles` instance to resize directly. +type ResizeFn = Box anyhow::Result<()> + Send>; + /// Handle for driving an interactive process (PTY or pipe). pub struct ProcessHandle { writer_tx: StdMutex>>>, @@ -82,6 +87,9 @@ pub struct ProcessHandle { // PtyHandles must be preserved because the process will receive Control+C if the // slave is closed _pty_handles: StdMutex>, + // Optional resize hook for driver-backed sessions that proxy PTY control to + // another backend instead of owning local PTY handles. + resizer: StdMutex>, } impl fmt::Debug for ProcessHandle { @@ -102,6 +110,7 @@ impl ProcessHandle { exit_status: Arc, exit_code: Arc>>, pty_handles: Option, + resizer: Option, ) -> Self { Self { writer_tx: StdMutex::new(Some(writer_tx)), @@ -113,6 +122,7 @@ impl ProcessHandle { exit_status, exit_code, _pty_handles: StdMutex::new(pty_handles), + resizer: StdMutex::new(resizer), } } @@ -141,17 +151,28 @@ impl ProcessHandle { /// Resize the PTY in character cells. pub fn resize(&self, size: TerminalSize) -> anyhow::Result<()> { - let handles = self - ._pty_handles + { + let handles = self + ._pty_handles + .lock() + .map_err(|_| anyhow!("failed to lock PTY handles"))?; + if let Some(handles) = handles.as_ref() { + return match &handles._master { + PtyMasterHandle::Resizable(master) => master.resize(size.into()), + #[cfg(unix)] + PtyMasterHandle::Opaque { raw_fd, .. } => resize_raw_pty(*raw_fd, size), + }; + } + } + + let mut resizer = self + .resizer .lock() - .map_err(|_| anyhow!("failed to lock PTY handles"))?; - let handles = handles - .as_ref() - .ok_or_else(|| anyhow!("process is not attached to a PTY"))?; - match &handles._master { - PtyMasterHandle::Resizable(master) => master.resize(size.into()), - #[cfg(unix)] - PtyMasterHandle::Opaque { raw_fd, .. } => resize_raw_pty(*raw_fd, size), + .map_err(|_| anyhow!("failed to lock PTY resizer"))?; + if let Some(resizer) = resizer.as_mut() { + resizer(size) + } else { + Err(anyhow!("process is not attached to a PTY")) } } @@ -205,6 +226,20 @@ impl Drop for ProcessHandle { } } +/// Adapts a closure into a `ChildTerminator` implementation. +struct ClosureTerminator { + inner: Option>, +} + +impl ChildTerminator for ClosureTerminator { + fn kill(&mut self) -> io::Result<()> { + if let Some(inner) = self.inner.as_mut() { + (inner)(); + } + Ok(()) + } +} + #[cfg(unix)] fn resize_raw_pty(raw_fd: RawFd, size: TerminalSize) -> anyhow::Result<()> { let mut winsize = libc::winsize { @@ -263,3 +298,113 @@ pub struct SpawnedProcess { pub stderr_rx: mpsc::Receiver>, pub exit_rx: oneshot::Receiver, } + +/// Driver-backed process handles for non-standard spawn backends. +pub struct ProcessDriver { + pub writer_tx: mpsc::Sender>, + pub stdout_rx: broadcast::Receiver>, + pub stderr_rx: Option>>, + pub exit_rx: oneshot::Receiver, + pub terminator: Option>, + pub writer_handle: Option>, + pub resizer: Option, +} + +/// Build a `SpawnedProcess` from a driver that supplies stdin/output/exit channels. +pub fn spawn_from_driver(driver: ProcessDriver) -> SpawnedProcess { + let ProcessDriver { + writer_tx, + stdout_rx: stdout_driver_rx, + stderr_rx: mut stderr_driver_rx, + exit_rx, + terminator, + writer_handle, + resizer, + } = driver; + + let (stdout_tx, stdout_rx) = mpsc::channel::>(256); + let (stderr_tx, stderr_rx) = mpsc::channel::>(256); + let (exit_seen_tx, exit_seen_rx) = watch::channel(false); + let spawn_stream_reader = + |mut output_rx: broadcast::Receiver>, + output_tx: mpsc::Sender>, + mut exit_seen_rx: watch::Receiver| { + tokio::spawn(async move { + let mut process_exited = false; + loop { + let recv_result = if process_exited { + match tokio::time::timeout( + std::time::Duration::from_millis(200), + output_rx.recv(), + ) + .await + { + Ok(result) => result, + Err(_) => break, + } + } else { + tokio::select! { + _ = exit_seen_rx.changed() => { + process_exited = *exit_seen_rx.borrow(); + continue; + } + result = output_rx.recv() => result, + } + }; + match recv_result { + Ok(chunk) => { + if output_tx.send(chunk).await.is_err() { + break; + } + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + } + } + }) + }; + let reader_handle = spawn_stream_reader(stdout_driver_rx, stdout_tx, exit_seen_rx.clone()); + let stderr_reader_handle = stderr_driver_rx + .take() + .map(|rx| spawn_stream_reader(rx, stderr_tx, exit_seen_rx)); + + let writer_handle = writer_handle.unwrap_or_else(|| tokio::spawn(async {})); + + let (exit_tx, exit_rx_out) = oneshot::channel::(); + let exit_status = Arc::new(AtomicBool::new(false)); + let wait_exit_status = Arc::clone(&exit_status); + let exit_code = Arc::new(StdMutex::new(None)); + let wait_exit_code = Arc::clone(&exit_code); + let wait_handle = tokio::spawn(async move { + let code = exit_rx.await.unwrap_or(-1); + wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst); + if let Ok(mut guard) = wait_exit_code.lock() { + *guard = Some(code); + } + let _ = exit_seen_tx.send(true); + let _ = exit_tx.send(code); + }); + + let handle = ProcessHandle::new( + writer_tx, + Box::new(ClosureTerminator { inner: terminator }), + reader_handle, + stderr_reader_handle + .map(|handle| handle.abort_handle()) + .into_iter() + .collect(), + writer_handle, + wait_handle, + exit_status, + exit_code, + /*pty_handles*/ None, + resizer, + ); + + SpawnedProcess { + session: handle, + stdout_rx, + stderr_rx, + exit_rx: exit_rx_out, + } +} diff --git a/codex-rs/utils/pty/src/pty.rs b/codex-rs/utils/pty/src/pty.rs index 4ae6facb7d..45c587b328 100644 --- a/codex-rs/utils/pty/src/pty.rs +++ b/codex-rs/utils/pty/src/pty.rs @@ -242,6 +242,7 @@ async fn spawn_process_portable( exit_status, exit_code, Some(handles), + /*resizer*/ None, ); Ok(SpawnedProcess { @@ -395,6 +396,7 @@ async fn spawn_process_preserving_fds( exit_status, exit_code, Some(handles), + /*resizer*/ None, ); Ok(SpawnedProcess { diff --git a/codex-rs/utils/pty/src/tests.rs b/codex-rs/utils/pty/src/tests.rs index c614f357ff..c7bfa512a3 100644 --- a/codex-rs/utils/pty/src/tests.rs +++ b/codex-rs/utils/pty/src/tests.rs @@ -3,6 +3,7 @@ use std::path::Path; use pretty_assertions::assert_eq; +use crate::ProcessDriver; use crate::SpawnedProcess; use crate::TerminalSize; use crate::combine_output_receivers; @@ -10,6 +11,7 @@ use crate::combine_output_receivers; use crate::pipe::spawn_process_no_stdin_with_inherited_fds; #[cfg(unix)] use crate::pty::spawn_process_with_inherited_fds; +use crate::spawn_from_driver; use crate::spawn_pipe_process; use crate::spawn_pipe_process_no_stdin; use crate::spawn_pty_process; @@ -589,6 +591,103 @@ async fn pipe_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn driver_backed_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()> { + let (writer_tx, _writer_rx) = tokio::sync::mpsc::channel::>(1); + let (stdout_tx, stdout_driver_rx) = tokio::sync::broadcast::channel::>(8); + let (stderr_tx, stderr_driver_rx) = tokio::sync::broadcast::channel::>(8); + let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::(); + + let spawned = spawn_from_driver(ProcessDriver { + writer_tx, + stdout_rx: stdout_driver_rx, + stderr_rx: Some(stderr_driver_rx), + exit_rx, + terminator: None, + writer_handle: None, + resizer: None, + }); + + let SpawnedProcess { + session: _session, + stdout_rx, + stderr_rx, + exit_rx, + } = spawned; + let stdout_task = tokio::spawn(async move { collect_split_output(stdout_rx).await }); + let stderr_task = tokio::spawn(async move { collect_split_output(stderr_rx).await }); + + stdout_tx.send(b"driver-out".to_vec())?; + stderr_tx.send(b"driver-err".to_vec())?; + drop(stdout_tx); + drop(stderr_tx); + exit_tx.send(0).expect("send exit code"); + + let timeout = tokio::time::Duration::from_secs(2); + let code = tokio::time::timeout(timeout, exit_rx) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for driver exit"))? + .unwrap_or(-1); + let stdout = tokio::time::timeout(timeout, stdout_task) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting to drain driver stdout"))??; + let stderr = tokio::time::timeout(timeout, stderr_task) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting to drain driver stderr"))??; + + assert_eq!(stdout, b"driver-out".to_vec()); + assert_eq!(stderr, b"driver-err".to_vec()); + assert_eq!(code, 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn driver_backed_process_can_resize_via_resizer_hook() -> anyhow::Result<()> { + let (writer_tx, _writer_rx) = tokio::sync::mpsc::channel::>(1); + let (_stdout_tx, stdout_driver_rx) = tokio::sync::broadcast::channel::>(8); + let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::(); + let (size_tx, size_rx) = tokio::sync::oneshot::channel::(); + + let size_tx = std::sync::Arc::new(std::sync::Mutex::new(Some(size_tx))); + let spawned = spawn_from_driver(ProcessDriver { + writer_tx, + stdout_rx: stdout_driver_rx, + stderr_rx: None, + exit_rx, + terminator: None, + writer_handle: None, + resizer: Some(Box::new(move |size| { + if let Ok(mut guard) = size_tx.lock() + && let Some(size_tx) = guard.take() + { + let _ = size_tx.send(size); + } + Ok(()) + })), + }); + + spawned.session.resize(TerminalSize { + rows: 40, + cols: 120, + })?; + exit_tx.send(0).expect("send exit code"); + + let resized = tokio::time::timeout(tokio::time::Duration::from_secs(2), size_rx) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for resize"))? + .expect("receive resized terminal size"); + assert_eq!( + resized, + TerminalSize { + rows: 40, + cols: 120 + } + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn pipe_terminate_aborts_detached_readers() -> anyhow::Result<()> { if !setsid_available() { diff --git a/codex-rs/utils/pty/src/win/psuedocon.rs b/codex-rs/utils/pty/src/win/psuedocon.rs index 3476665633..f235c70278 100644 --- a/codex-rs/utils/pty/src/win/psuedocon.rs +++ b/codex-rs/utils/pty/src/win/psuedocon.rs @@ -118,6 +118,10 @@ fn windows_build_number() -> Option { pub struct PsuedoCon { con: HPCON, + // CreatePseudoConsole borrows these pipe handles for the lifetime of the + // pseudoconsole, so we must keep owning them until ClosePseudoConsole. + _input: FileDescriptor, + _output: FileDescriptor, } unsafe impl Send for PsuedoCon {} @@ -149,7 +153,11 @@ impl PsuedoCon { result == S_OK, "failed to create psuedo console: HRESULT {result}" ); - Ok(Self { con }) + Ok(Self { + con, + _input: input, + _output: output, + }) } pub fn resize(&self, size: COORD) -> Result<(), Error> { diff --git a/codex-rs/windows-sandbox-rs/src/conpty/mod.rs b/codex-rs/windows-sandbox-rs/src/conpty/mod.rs index c7b992f518..54d1f34281 100644 --- a/codex-rs/windows-sandbox-rs/src/conpty/mod.rs +++ b/codex-rs/windows-sandbox-rs/src/conpty/mod.rs @@ -6,10 +6,8 @@ //! `tty=true`. The helpers are not tied to the IPC layer and can be reused by other //! Windows sandbox flows that need a PTY. -mod proc_thread_attr; - -use self::proc_thread_attr::ProcThreadAttributeList; use crate::desktop::LaunchDesktop; +use crate::proc_thread_attr::ProcThreadAttributeList; use crate::winutil::format_last_error; use crate::winutil::quote_windows_arg; use crate::winutil::to_wide; @@ -37,7 +35,7 @@ pub struct ConptyInstance { pub hpc: HANDLE, pub input_write: HANDLE, pub output_read: HANDLE, - _desktop: LaunchDesktop, + desktop: Option, } impl Drop for ConptyInstance { @@ -58,9 +56,10 @@ impl Drop for ConptyInstance { impl ConptyInstance { /// Consume the instance and return raw handles without closing them. - pub fn into_raw(self) -> (HANDLE, HANDLE, HANDLE) { + pub fn into_raw(self) -> (HANDLE, HANDLE, HANDLE, Option) { let me = std::mem::ManuallyDrop::new(self); - (me.hpc, me.input_write, me.output_read) + let desktop = unsafe { std::ptr::read(&me.desktop) }; + (me.hpc, me.input_write, me.output_read, desktop) } } @@ -68,6 +67,7 @@ impl ConptyInstance { /// /// This is public so callers that need lower-level PTY setup can build on the same /// primitive, although the common entry point is `spawn_conpty_process_as_user`. +#[allow(dead_code)] pub fn create_conpty(cols: i16, rows: i16) -> Result { let raw = RawConPty::new(cols, rows)?; let (hpc, input_write, output_read) = raw.into_raw_handles(); @@ -76,9 +76,7 @@ pub fn create_conpty(cols: i16, rows: i16) -> Result { hpc: hpc as HANDLE, input_write: input_write as HANDLE, output_read: output_read as HANDLE, - _desktop: LaunchDesktop::prepare( - /*use_private_desktop*/ false, /*logs_base_dir*/ None, - )?, + desktop: None, }) } @@ -110,7 +108,14 @@ pub fn spawn_conpty_process_as_user( let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?; si.StartupInfo.lpDesktop = desktop.startup_info_desktop(); - let conpty = create_conpty(/*cols*/ 80, /*rows*/ 24)?; + let raw = RawConPty::new(/*cols*/ 80, /*rows*/ 24)?; + let (hpc, input_write, output_read) = raw.into_raw_handles(); + let conpty = ConptyInstance { + hpc: hpc as HANDLE, + input_write: input_write as HANDLE, + output_read: output_read as HANDLE, + desktop: Some(desktop), + }; let mut attrs = ProcThreadAttributeList::new(/*attr_count*/ 1)?; attrs.set_pseudoconsole(conpty.hpc)?; si.lpAttributeList = attrs.as_mut_ptr(); @@ -142,7 +147,5 @@ pub fn spawn_conpty_process_as_user( env_block.len() )); } - let mut conpty = conpty; - conpty._desktop = desktop; Ok((pi, conpty)) } diff --git a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs index 9cb59e18dc..dcca40abff 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/command_runner_win.rs @@ -15,10 +15,12 @@ use anyhow::Result; use codex_windows_sandbox::ErrorPayload; use codex_windows_sandbox::ExitPayload; use codex_windows_sandbox::FramedMessage; +use codex_windows_sandbox::LaunchDesktop; use codex_windows_sandbox::Message; use codex_windows_sandbox::OutputPayload; use codex_windows_sandbox::OutputStream; use codex_windows_sandbox::PipeSpawnHandles; +use codex_windows_sandbox::ResizePayload; use codex_windows_sandbox::SandboxPolicy; use codex_windows_sandbox::SpawnReady; use codex_windows_sandbox::SpawnRequest; @@ -56,7 +58,9 @@ use windows_sys::Win32::Storage::FileSystem::CreateFileW; use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ; use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE; use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING; +use windows_sys::Win32::System::Console::COORD; use windows_sys::Win32::System::Console::ClosePseudoConsole; +use windows_sys::Win32::System::Console::ResizePseudoConsole; use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject; use windows_sys::Win32::System::JobObjects::CreateJobObjectW; use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; @@ -86,6 +90,8 @@ struct IpcSpawnedProcess { stderr_handle: HANDLE, stdin_handle: Option, hpc_handle: Option, + _desktop_owner: Option, + _pipe_handles: Option, } unsafe fn create_job_kill_on_close() -> Result { @@ -166,7 +172,7 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { Err(err) => { log_note( &format!( - "junction: read_acl_mutex_exists failed: {err}; assuming read ACL helper is running" + "junction: failed to probe ACL mutex state: {err}; defaulting to junction cwd" ), log_dir, ); @@ -174,10 +180,6 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { } }; if use_junction { - log_note( - "junction: read ACL helper running; using junction CWD", - log_dir, - ); cwd_junction::create_cwd_junction(req_cwd, log_dir).unwrap_or_else(|| req_cwd.to_path_buf()) } else { req_cwd.to_path_buf() @@ -187,16 +189,6 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf { fn spawn_ipc_process(req: &SpawnRequest) -> Result { let log_dir = req.codex_home.clone(); hide_current_user_profile_dir(req.codex_home.as_path()); - log_note( - &format!( - "runner start cwd={} cmd={:?} real_codex_home={}", - req.cwd.display(), - req.command, - req.real_codex_home.display() - ), - Some(&req.codex_home), - ); - let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; let mut cap_psids: Vec<*mut c_void> = Vec::new(); for sid in &req.cap_sids { @@ -240,16 +232,10 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { } let effective_cwd = effective_cwd(&req.cwd, Some(log_dir.as_path())); - log_note( - &format!( - "runner: effective cwd={} (requested {})", - effective_cwd.display(), - req.cwd.display() - ), - Some(log_dir.as_path()), - ); let mut hpc_handle: Option = None; + let mut desktop_owner = None; + let mut pipe_handles = None; let (pi, stdout_handle, stderr_handle, stdin_handle) = if req.tty { let (pi, conpty) = codex_windows_sandbox::spawn_conpty_process_as_user( h_token, @@ -259,8 +245,9 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { req.use_private_desktop, Some(log_dir.as_path()), )?; - let (hpc, input_write, output_read) = conpty.into_raw(); + let (hpc, input_write, output_read, desktop) = conpty.into_raw(); hpc_handle = Some(hpc); + desktop_owner = desktop; let stdin_handle = if req.stdin_open { Some(input_write) } else { @@ -281,29 +268,29 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { } else { StdinMode::Closed }; - let pipe_handles: PipeSpawnHandles = spawn_process_with_pipes( + let spawned_pipes: PipeSpawnHandles = spawn_process_with_pipes( h_token, &req.command, &effective_cwd, &req.env, stdin_mode, StderrMode::Separate, - /*use_private_desktop*/ false, + req.use_private_desktop, + Some(log_dir.as_path()), )?; - ( - pipe_handles.process, - pipe_handles.stdout_read, - pipe_handles - .stderr_read - .unwrap_or(windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE), - pipe_handles.stdin_write, - ) + let pi = spawned_pipes.process; + let stdout_handle = spawned_pipes.stdout_read; + let stderr_handle = spawned_pipes + .stderr_read + .unwrap_or(windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE); + let stdin_handle = spawned_pipes.stdin_write; + pipe_handles = Some(spawned_pipes); + (pi, stdout_handle, stderr_handle, stdin_handle) }; unsafe { CloseHandle(h_token); } - Ok(IpcSpawnedProcess { log_dir, pi, @@ -311,6 +298,8 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { stderr_handle, stdin_handle, hpc_handle, + _desktop_owner: desktop_owner, + _pipe_handles: pipe_handles, }) } @@ -346,21 +335,17 @@ fn spawn_output_reader( fn spawn_input_loop( mut reader: File, stdin_handle: Option, + hpc_handle: Arc>>, process_handle: Arc>>, - log_dir: Option, + _log_dir: Option, ) -> std::thread::JoinHandle<()> { std::thread::spawn(move || { + let mut stdin_handle = stdin_handle; loop { let msg = match read_frame(&mut reader) { Ok(Some(v)) => v, Ok(None) => break, - Err(err) => { - log_note( - &format!("runner input read failed: {err}"), - log_dir.as_deref(), - ); - break; - } + Err(_) => break, }; match msg.message { Message::Stdin { payload } => { @@ -380,6 +365,30 @@ fn spawn_input_loop( } } } + Message::CloseStdin { .. } => { + if let Some(handle) = stdin_handle.take() { + unsafe { + CloseHandle(handle); + } + } + } + Message::Resize { + payload: ResizePayload { rows, cols }, + } => { + if let Ok(guard) = hpc_handle.lock() + && let Some(hpc) = guard.as_ref() + { + unsafe { + let _ = ResizePseudoConsole( + *hpc, + COORD { + X: cols as i16, + Y: rows as i16, + }, + ); + } + } + } Message::Terminate { .. } => { if let Ok(guard) = process_handle.lock() && let Some(handle) = guard.as_ref() @@ -450,7 +459,7 @@ pub fn main() -> Result<()> { let stdout_handle = ipc_spawn.stdout_handle; let stderr_handle = ipc_spawn.stderr_handle; let stdin_handle = ipc_spawn.stdin_handle; - let hpc_handle = ipc_spawn.hpc_handle; + let hpc_handle = Arc::new(StdMutex::new(ipc_spawn.hpc_handle)); let h_job = unsafe { create_job_kill_on_close().ok() }; if let Some(job) = h_job { @@ -474,7 +483,6 @@ pub fn main() -> Result<()> { } else { anyhow::bail!("runner spawn_ready write failed: pipe_write lock poisoned"); } { - log_note(&format!("runner spawn_ready write failed: {err}"), log_dir); let _ = send_error(&pipe_write, "spawn_failed", err.to_string()); return Err(err); } @@ -499,6 +507,7 @@ pub fn main() -> Result<()> { let _input_thread = spawn_input_loop( pipe_read, stdin_handle, + Arc::clone(&hpc_handle), Arc::clone(&process_handle), log_dir_owned, ); @@ -517,9 +526,6 @@ pub fn main() -> Result<()> { GetExitCodeProcess(pi.hProcess, &mut raw_exit); exit_code = raw_exit as i32; } - if let Some(hpc) = hpc_handle { - ClosePseudoConsole(hpc); - } if pi.hThread != 0 { CloseHandle(pi.hThread); } @@ -530,10 +536,20 @@ pub fn main() -> Result<()> { CloseHandle(job); } } - let _ = out_thread.join(); - if let Some(err_thread) = err_thread { - let _ = err_thread.join(); + + if let Ok(mut guard) = hpc_handle.lock() + && let Some(hpc) = guard.take() + { + unsafe { + ClosePseudoConsole(hpc); + } } + + let _ = out_thread.join(); + if let Some(thread) = err_thread { + let _ = thread.join(); + } + let exit_msg = FramedMessage { version: 1, message: Message::Exit { diff --git a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs index 3f3425eb0e..56e33e57ca 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs @@ -33,8 +33,8 @@ pub struct FramedMessage { /// IPC message variants exchanged between parent and runner. /// -/// `SpawnRequest`, `Stdin`, and `Terminate` are parent->runner commands. `SpawnReady`, -/// `Output`, `Exit`, and `Error` are runner->parent events/results. +/// `SpawnRequest`, `Stdin`, `CloseStdin`, `Resize`, and `Terminate` are parent->runner commands. +/// `SpawnReady`, `Output`, `Exit`, and `Error` are runner->parent events/results. #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Message { @@ -42,6 +42,8 @@ pub enum Message { SpawnReady { payload: SpawnReady }, Output { payload: OutputPayload }, Stdin { payload: StdinPayload }, + CloseStdin { payload: EmptyPayload }, + Resize { payload: ResizePayload }, Exit { payload: ExitPayload }, Error { payload: ErrorPayload }, Terminate { payload: EmptyPayload }, @@ -93,6 +95,13 @@ pub struct StdinPayload { pub data_b64: String, } +/// PTY resize request sent from parent to runner. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ResizePayload { + pub rows: u16, + pub cols: u16, +} + /// Exit status sent from runner to parent. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ExitPayload { diff --git a/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs new file mode 100644 index 0000000000..bbcaec330f --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs @@ -0,0 +1,213 @@ +use crate::identity::SandboxCreds; +use crate::ipc_framed::FramedMessage; +use crate::ipc_framed::Message; +use crate::ipc_framed::SpawnRequest; +use crate::ipc_framed::read_frame; +use crate::ipc_framed::write_frame; +use crate::runner_pipe::PIPE_ACCESS_INBOUND; +use crate::runner_pipe::PIPE_ACCESS_OUTBOUND; +use crate::runner_pipe::connect_pipe; +use crate::runner_pipe::create_named_pipe; +use crate::runner_pipe::find_runner_exe; +use crate::runner_pipe::pipe_pair; +use crate::winutil::quote_windows_arg; +use crate::winutil::to_wide; +use anyhow::Result; +use std::ffi::c_void; +use std::fs::File; +use std::os::windows::io::AsRawHandle; +use std::os::windows::io::FromRawHandle; +use std::path::Path; +use std::ptr; +use std::time::Duration; +use std::time::Instant; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode; +use windows_sys::Win32::System::Pipes::PeekNamedPipe; +use windows_sys::Win32::System::Threading::CreateProcessWithLogonW; +use windows_sys::Win32::System::Threading::LOGON_WITH_PROFILE; +use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; +use windows_sys::Win32::System::Threading::STARTUPINFOW; + +const RUNNER_SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(15); +const RUNNER_SPAWN_READY_POLL_INTERVAL: Duration = Duration::from_millis(50); +const RUNNER_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002; + +pub(crate) struct RunnerTransport { + pipe_write: File, + pipe_read: File, +} + +impl RunnerTransport { + pub(crate) fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()> { + let spawn_request = FramedMessage { + version: 1, + message: Message::SpawnRequest { + payload: Box::new(request), + }, + }; + write_frame(&mut self.pipe_write, &spawn_request) + } + + pub(crate) fn read_spawn_ready(&mut self) -> Result<()> { + wait_for_complete_frame(&self.pipe_read, RUNNER_SPAWN_READY_TIMEOUT)?; + let msg = read_frame(&mut self.pipe_read)? + .ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?; + match msg.message { + Message::SpawnReady { .. } => Ok(()), + Message::Error { payload } => Err(anyhow::anyhow!("runner error: {}", payload.message)), + other => Err(anyhow::anyhow!( + "expected spawn_ready from runner, got {other:?}" + )), + } + } + + pub(crate) fn into_files(self) -> (File, File) { + (self.pipe_write, self.pipe_read) + } +} + +pub(crate) fn spawn_runner_transport( + codex_home: &Path, + cwd: &Path, + sandbox_creds: &SandboxCreds, + log_dir: Option<&Path>, +) -> Result { + let (pipe_in_name, pipe_out_name) = pipe_pair(); + let h_pipe_in = + create_named_pipe(&pipe_in_name, PIPE_ACCESS_OUTBOUND, &sandbox_creds.username)?; + let h_pipe_out = + create_named_pipe(&pipe_out_name, PIPE_ACCESS_INBOUND, &sandbox_creds.username)?; + + let runner_exe = find_runner_exe(codex_home, log_dir); + let runner_cmdline = runner_exe + .to_str() + .map(str::to_owned) + .unwrap_or_else(|| "codex-command-runner.exe".to_string()); + let runner_full_cmd = format!( + "{} {} {}", + quote_windows_arg(&runner_cmdline), + quote_windows_arg(&format!("--pipe-in={pipe_in_name}")), + quote_windows_arg(&format!("--pipe-out={pipe_out_name}")) + ); + let mut cmdline_vec = to_wide(&runner_full_cmd); + let exe_w = to_wide(&runner_cmdline); + let cwd_w = to_wide(cwd); + let user_w = to_wide(&sandbox_creds.username); + let domain_w = to_wide("."); + let password_w = to_wide(&sandbox_creds.password); + let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; + si.cb = std::mem::size_of::() as u32; + let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + let env_block: Option> = None; + + let previous_error_mode = unsafe { SetErrorMode(RUNNER_ERROR_MODE_FLAGS) }; + let spawn_res = unsafe { + CreateProcessWithLogonW( + user_w.as_ptr(), + domain_w.as_ptr(), + password_w.as_ptr(), + LOGON_WITH_PROFILE, + exe_w.as_ptr(), + cmdline_vec.as_mut_ptr(), + windows_sys::Win32::System::Threading::CREATE_NO_WINDOW + | windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT, + env_block + .as_ref() + .map(|block| block.as_ptr() as *const c_void) + .unwrap_or(ptr::null()), + cwd_w.as_ptr(), + &si, + &mut pi, + ) + }; + unsafe { + SetErrorMode(previous_error_mode); + } + if spawn_res == 0 { + let err = unsafe { GetLastError() } as i32; + unsafe { + CloseHandle(h_pipe_in); + CloseHandle(h_pipe_out); + } + return Err(anyhow::anyhow!("CreateProcessWithLogonW failed: {err}")); + } + + let connect_result = (|| -> Result<()> { + connect_pipe(h_pipe_in)?; + connect_pipe(h_pipe_out)?; + Ok(()) + })(); + + unsafe { + if pi.hThread != 0 { + CloseHandle(pi.hThread); + } + if pi.hProcess != 0 { + CloseHandle(pi.hProcess); + } + } + + if let Err(err) = connect_result { + unsafe { + CloseHandle(h_pipe_in); + CloseHandle(h_pipe_out); + } + return Err(err); + } + + let pipe_write = unsafe { File::from_raw_handle(h_pipe_in as _) }; + let pipe_read = unsafe { File::from_raw_handle(h_pipe_out as _) }; + Ok(RunnerTransport { + pipe_write, + pipe_read, + }) +} + +fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> { + let handle = pipe_read.as_raw_handle() as HANDLE; + let deadline = Instant::now() + timeout; + let mut len_buf = [0u8; 4]; + + loop { + let mut bytes_read = 0u32; + let mut total_available = 0u32; + let ok = unsafe { + PeekNamedPipe( + handle, + len_buf.as_mut_ptr() as *mut c_void, + len_buf.len() as u32, + &mut bytes_read, + &mut total_available, + ptr::null_mut(), + ) + }; + if ok == 0 { + let err = unsafe { GetLastError() } as i32; + return Err(anyhow::anyhow!( + "PeekNamedPipe failed while waiting for spawn_ready: {err}" + )); + } + + if bytes_read == len_buf.len() as u32 { + let frame_len = u32::from_le_bytes(len_buf) as usize; + let total_len = frame_len + .checked_add(len_buf.len()) + .ok_or_else(|| anyhow::anyhow!("runner frame length overflow"))?; + if total_available as usize >= total_len { + return Ok(()); + } + } + + if Instant::now() >= deadline { + return Err(anyhow::anyhow!( + "timed out after {}ms waiting for runner spawn_ready", + timeout.as_millis() + )); + } + + std::thread::sleep(RUNNER_SPAWN_READY_POLL_INTERVAL); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs b/codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs index 0bc74fa863..904c5102a5 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/runner_pipe.rs @@ -20,6 +20,8 @@ use std::path::PathBuf; use std::ptr; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::Foundation::LocalFree; use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR; use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; @@ -43,7 +45,8 @@ pub fn find_runner_exe(codex_home: &Path, log_dir: Option<&Path>) -> PathBuf { /// Generates a unique named-pipe path used to communicate with the runner process. pub fn pipe_pair() -> (String, String) { let mut rng = SmallRng::from_entropy(); - let base = format!(r"\\.\pipe\codex-runner-{:x}", rng.gen::()); + let nonce: u128 = rng.r#gen(); + let base = format!(r"\\.\pipe\codex-runner-{nonce:x}"); (format!("{base}-in"), format!("{base}-out")) } @@ -86,6 +89,9 @@ pub fn create_named_pipe(name: &str, access: u32, sandbox_username: &str) -> io: &mut sa as *mut SECURITY_ATTRIBUTES, ) }; + unsafe { + LocalFree(sd as HLOCAL); + } if h == 0 || h == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE { return Err(io::Error::from_raw_os_error(unsafe { GetLastError() as i32 diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 3b9601b16f..b807ded589 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -35,6 +35,10 @@ windows_modules!( #[path = "conpty/mod.rs"] mod conpty; +#[cfg(target_os = "windows")] +#[path = "proc_thread_attr.rs"] +mod proc_thread_attr; + #[cfg(target_os = "windows")] #[path = "elevated/ipc_framed.rs"] pub(crate) mod ipc_framed; @@ -46,9 +50,29 @@ mod setup; #[cfg(target_os = "windows")] mod elevated_impl; +#[cfg(target_os = "windows")] +#[path = "elevated/runner_pipe.rs"] +mod runner_pipe; + +#[cfg(target_os = "windows")] +#[path = "elevated/runner_client.rs"] +mod runner_client; + #[cfg(target_os = "windows")] mod setup_error; +#[cfg(target_os = "windows")] +#[path = "sandbox_utils.rs"] +mod sandbox_utils; + +#[cfg(target_os = "windows")] +#[path = "spawn_prep.rs"] +mod spawn_prep; + +#[cfg(target_os = "windows")] +#[path = "unified_exec/session.rs"] +mod session; + #[cfg(target_os = "windows")] pub use acl::add_deny_write_ace; @@ -73,6 +97,8 @@ pub use cap::workspace_cap_sid_for_cwd; #[cfg(target_os = "windows")] pub use conpty::spawn_conpty_process_as_user; #[cfg(target_os = "windows")] +pub use desktop::LaunchDesktop; +#[cfg(target_os = "windows")] pub use dpapi::protect as dpapi_protect; #[cfg(target_os = "windows")] pub use dpapi::unprotect as dpapi_unprotect; @@ -103,6 +129,8 @@ pub use ipc_framed::OutputPayload; #[cfg(target_os = "windows")] pub use ipc_framed::OutputStream; #[cfg(target_os = "windows")] +pub use ipc_framed::ResizePayload; +#[cfg(target_os = "windows")] pub use ipc_framed::SpawnReady; #[cfg(target_os = "windows")] pub use ipc_framed::SpawnRequest; @@ -137,6 +165,10 @@ pub use process::read_handle_loop; #[cfg(target_os = "windows")] pub use process::spawn_process_with_pipes; #[cfg(target_os = "windows")] +pub use session::spawn_windows_sandbox_session_elevated; +#[cfg(target_os = "windows")] +pub use session::spawn_windows_sandbox_session_legacy; +#[cfg(target_os = "windows")] pub use setup::SETUP_VERSION; #[cfg(target_os = "windows")] pub use setup::SandboxSetupRequest; @@ -214,16 +246,13 @@ mod windows_impl { use super::allow::compute_allow_paths; use super::cap::load_or_create_cap_sids; use super::cap::workspace_cap_sid_for_cwd; - use super::env::apply_no_network_to_env; - use super::env::ensure_non_interactive_pager; - use super::env::normalize_null_device_env; use super::logging::log_failure; - use super::logging::log_start; use super::logging::log_success; use super::path_normalization::canonicalize_path; use super::policy::SandboxPolicy; - use super::policy::parse_policy; use super::process::create_process_as_user; + use super::sandbox_utils::ensure_codex_home_exists; + use super::spawn_prep::prepare_legacy_spawn_context; use super::token::convert_string_sid_to_sid; use super::token::create_workspace_write_token_with_caps_from; use super::workspace_acl::is_command_cwd_root; @@ -246,15 +275,6 @@ mod windows_impl { type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE)); - fn should_apply_network_block(policy: &SandboxPolicy) -> bool { - !policy.has_full_network_access() - } - - fn ensure_codex_home_exists(p: &Path) -> Result<()> { - std::fs::create_dir_all(p)?; - Ok(()) - } - unsafe fn setup_stdio_pipes() -> io::Result { let mut in_r: HANDLE = 0; let mut in_w: HANDLE = 0; @@ -326,27 +346,19 @@ mod windows_impl { additional_deny_write_paths: &[PathBuf], use_private_desktop: bool, ) -> Result { - let policy = parse_policy(policy_json_or_preset)?; - let apply_network_block = should_apply_network_block(&policy); - normalize_null_device_env(&mut env_map); - ensure_non_interactive_pager(&mut env_map); - if apply_network_block { - apply_no_network_to_env(&mut env_map)?; - } - ensure_codex_home_exists(codex_home)?; - let current_dir = cwd.to_path_buf(); - let sandbox_base = codex_home.join(".sandbox"); - std::fs::create_dir_all(&sandbox_base)?; - let logs_base_dir = Some(sandbox_base.as_path()); - log_start(&command, logs_base_dir); - let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. }); - - if matches!( - &policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { - anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") - } + let common = prepare_legacy_spawn_context( + policy_json_or_preset, + codex_home, + cwd, + &mut env_map, + &command, + /*inherit_path*/ false, + /*add_git_safe_directory*/ false, + )?; + let policy = common.policy; + let current_dir = common.current_dir; + let logs_base_dir = common.logs_base_dir.as_deref(); + let is_workspace_write = common.is_workspace_write; if !policy.has_full_disk_read_access() { anyhow::bail!( "Restricted read-only access requires the elevated Windows sandbox backend" @@ -440,7 +452,6 @@ mod windows_impl { allow_null_device(psid); } } - let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? }; let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair); let spawn_res = unsafe { @@ -571,7 +582,6 @@ mod windows_impl { } } } - Ok(CaptureResult { exit_code, stdout, @@ -605,7 +615,6 @@ mod windows_impl { let AllowDenyPaths { allow, deny } = compute_allow_paths(sandbox_policy, sandbox_policy_cwd, ¤t_dir, env_map); let canonical_cwd = canonicalize_path(¤t_dir); - unsafe { for p in &allow { let psid = if is_command_cwd_root(p, &canonical_cwd) { @@ -627,8 +636,8 @@ mod windows_impl { #[cfg(test)] mod tests { - use super::should_apply_network_block; use crate::policy::SandboxPolicy; + use crate::spawn_prep::should_apply_network_block; fn workspace_policy(network_access: bool) -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { diff --git a/codex-rs/windows-sandbox-rs/src/conpty/proc_thread_attr.rs b/codex-rs/windows-sandbox-rs/src/proc_thread_attr.rs similarity index 65% rename from codex-rs/windows-sandbox-rs/src/conpty/proc_thread_attr.rs rename to codex-rs/windows-sandbox-rs/src/proc_thread_attr.rs index 393172a0f8..7641bb5edd 100644 --- a/codex-rs/windows-sandbox-rs/src/conpty/proc_thread_attr.rs +++ b/codex-rs/windows-sandbox-rs/src/proc_thread_attr.rs @@ -1,25 +1,20 @@ -//! Low-level Windows thread attribute helpers used by ConPTY spawn. -//! -//! This module wraps the Win32 `PROC_THREAD_ATTRIBUTE_LIST` APIs so ConPTY handles can -//! be attached to a child process. It is ConPTY‑specific and used in both legacy and -//! elevated unified_exec paths when spawning a PTY‑backed process. - use std::io; use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::System::Threading::DeleteProcThreadAttributeList; use windows_sys::Win32::System::Threading::InitializeProcThreadAttributeList; use windows_sys::Win32::System::Threading::LPPROC_THREAD_ATTRIBUTE_LIST; use windows_sys::Win32::System::Threading::UpdateProcThreadAttribute; -const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; +const PROC_THREAD_ATTRIBUTE_HANDLE_LIST: usize = 0x0002_0002; +const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x0002_0016; -/// RAII wrapper for Windows PROC_THREAD_ATTRIBUTE_LIST. pub struct ProcThreadAttributeList { buffer: Vec, + handle_list: Option>, } impl ProcThreadAttributeList { - /// Allocate and initialize a thread attribute list. pub fn new(attr_count: u32) -> io::Result { let mut size: usize = 0; unsafe { @@ -38,15 +33,16 @@ impl ProcThreadAttributeList { GetLastError() as i32 })); } - Ok(Self { buffer }) + Ok(Self { + buffer, + handle_list: None, + }) } - /// Return a mutable pointer to the attribute list for Win32 APIs. pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { self.buffer.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST } - /// Attach a ConPTY handle to the attribute list. pub fn set_pseudoconsole(&mut self, hpc: isize) -> io::Result<()> { let list = self.as_mut_ptr(); let mut hpc_value = hpc; @@ -68,6 +64,31 @@ impl ProcThreadAttributeList { } Ok(()) } + + pub fn set_handle_list(&mut self, handles: Vec) -> io::Result<()> { + self.handle_list = Some(handles); + let list = self.as_mut_ptr(); + let Some(handle_list) = self.handle_list.as_mut() else { + return Err(io::Error::other("handle list missing after initialization")); + }; + let ok = unsafe { + UpdateProcThreadAttribute( + list, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handle_list.as_mut_ptr().cast(), + std::mem::size_of_val(handle_list.as_slice()), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + }; + if ok == 0 { + return Err(io::Error::from_raw_os_error(unsafe { + GetLastError() as i32 + })); + } + Ok(()) + } } impl Drop for ProcThreadAttributeList { diff --git a/codex-rs/windows-sandbox-rs/src/process.rs b/codex-rs/windows-sandbox-rs/src/process.rs index ec31b06b5f..1571bb8ccf 100644 --- a/codex-rs/windows-sandbox-rs/src/process.rs +++ b/codex-rs/windows-sandbox-rs/src/process.rs @@ -1,7 +1,8 @@ use crate::desktop::LaunchDesktop; use crate::logging; +use crate::proc_thread_attr::ProcThreadAttributeList; +use crate::winutil::argv_to_command_line; use crate::winutil::format_last_error; -use crate::winutil::quote_windows_arg; use crate::winutil::to_wide; use anyhow::anyhow; use anyhow::Result; @@ -23,8 +24,10 @@ use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; use windows_sys::Win32::System::Pipes::CreatePipe; use windows_sys::Win32::System::Threading::CreateProcessAsUserW; use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; +use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT; use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES; +use windows_sys::Win32::System::Threading::STARTUPINFOEXW; use windows_sys::Win32::System::Threading::STARTUPINFOW; pub struct CreatedProcess { @@ -81,81 +84,118 @@ pub unsafe fn create_process_as_user( stdio: Option<(HANDLE, HANDLE, HANDLE)>, use_private_desktop: bool, ) -> Result { - let cmdline_str = argv - .iter() - .map(|a| quote_windows_arg(a)) - .collect::>() - .join(" "); + let cmdline_str = argv_to_command_line(argv); let mut cmdline: Vec = to_wide(&cmdline_str); let env_block = make_env_block(env_map); - let mut si: STARTUPINFOW = std::mem::zeroed(); - si.cb = std::mem::size_of::() as u32; - // Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED - // if lpDesktop is not set when launching with a restricted token. - // Point explicitly at the interactive desktop or a private desktop. let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?; - si.lpDesktop = desktop.startup_info_desktop(); let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); - // Ensure handles are inheritable when custom stdio is supplied. - let inherit_handles = match stdio { + let cwd_wide = to_wide(cwd); + let env_block_len = env_block.len(); + match stdio { Some((stdin_h, stdout_h, stderr_h)) => { - si.dwFlags |= STARTF_USESTDHANDLES; - si.hStdInput = stdin_h; - si.hStdOutput = stdout_h; - si.hStdError = stderr_h; - for h in [stdin_h, stdout_h, stderr_h] { - if SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 { + let mut si: STARTUPINFOEXW = std::mem::zeroed(); + si.StartupInfo.cb = std::mem::size_of::() as u32; + // Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED + // if lpDesktop is not set when launching with a restricted token. + // Point explicitly at the interactive desktop or a private desktop. + si.StartupInfo.lpDesktop = desktop.startup_info_desktop(); + si.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + si.StartupInfo.hStdInput = stdin_h; + si.StartupInfo.hStdOutput = stdout_h; + si.StartupInfo.hStdError = stderr_h; + let mut inherited_handles = vec![stdin_h, stdout_h]; + if !inherited_handles.contains(&stderr_h) { + inherited_handles.push(stderr_h); + } + for &handle in &inherited_handles { + if SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 { return Err(anyhow!( "SetHandleInformation failed for stdio handle: {}", GetLastError() )); } } - true + let mut attrs = ProcThreadAttributeList::new(/*attr_count*/ 1)?; + attrs.set_handle_list(inherited_handles)?; + si.lpAttributeList = attrs.as_mut_ptr(); + + let creation_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT; + let ok = CreateProcessAsUserW( + h_token, + std::ptr::null(), + cmdline.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 1, + creation_flags, + env_block.as_ptr() as *mut c_void, + cwd_wide.as_ptr(), + &si.StartupInfo, + &mut pi, + ); + if ok == 0 { + let err = GetLastError() as i32; + let msg = format!( + "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", + err, + format_last_error(err), + cwd.display(), + cmdline_str, + env_block_len, + si.StartupInfo.dwFlags, + creation_flags, + ); + logging::debug_log(&msg, logs_base_dir); + return Err(anyhow!("CreateProcessAsUserW failed: {err}")); + } + Ok(CreatedProcess { + process_info: pi, + startup_info: si.StartupInfo, + _desktop: desktop, + }) } None => { + let mut si: STARTUPINFOW = std::mem::zeroed(); + si.cb = std::mem::size_of::() as u32; + si.lpDesktop = desktop.startup_info_desktop(); ensure_inheritable_stdio(&mut si)?; - true + + let creation_flags = CREATE_UNICODE_ENVIRONMENT; + let ok = CreateProcessAsUserW( + h_token, + std::ptr::null(), + cmdline.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 1, + creation_flags, + env_block.as_ptr() as *mut c_void, + cwd_wide.as_ptr(), + &si, + &mut pi, + ); + if ok == 0 { + let err = GetLastError() as i32; + let msg = format!( + "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", + err, + format_last_error(err), + cwd.display(), + cmdline_str, + env_block_len, + si.dwFlags, + creation_flags, + ); + logging::debug_log(&msg, logs_base_dir); + return Err(anyhow!("CreateProcessAsUserW failed: {err}")); + } + Ok(CreatedProcess { + process_info: pi, + startup_info: si, + _desktop: desktop, + }) } - }; - - let creation_flags = CREATE_UNICODE_ENVIRONMENT; - let cwd_wide = to_wide(cwd); - let env_block_len = env_block.len(); - - let ok = CreateProcessAsUserW( - h_token, - std::ptr::null(), - cmdline.as_mut_ptr(), - std::ptr::null_mut(), - std::ptr::null_mut(), - inherit_handles as i32, - creation_flags, - env_block.as_ptr() as *mut c_void, - cwd_wide.as_ptr(), - &si, - &mut pi, - ); - if ok == 0 { - let err = GetLastError() as i32; - let msg = format!( - "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", - err, - format_last_error(err), - cwd.display(), - cmdline_str, - env_block_len, - si.dwFlags, - creation_flags, - ); - logging::debug_log(&msg, logs_base_dir); - return Err(anyhow!("CreateProcessAsUserW failed: {err}")); } - Ok(CreatedProcess { - process_info: pi, - startup_info: si, - _desktop: desktop, - }) } /// Controls whether the child's stdin handle is kept open for writing. @@ -179,9 +219,11 @@ pub struct PipeSpawnHandles { pub stdin_write: Option, pub stdout_read: HANDLE, pub stderr_read: Option, + pub(crate) desktop: LaunchDesktop, } /// Spawns a process with anonymous pipes and returns the relevant handles. +#[allow(clippy::too_many_arguments)] pub fn spawn_process_with_pipes( h_token: HANDLE, argv: &[String], @@ -190,6 +232,7 @@ pub fn spawn_process_with_pipes( stdin_mode: StdinMode, stderr_mode: StderrMode, use_private_desktop: bool, + logs_base_dir: Option<&Path>, ) -> Result { let mut in_r: HANDLE = 0; let mut in_w: HANDLE = 0; @@ -229,7 +272,7 @@ pub fn spawn_process_with_pipes( argv, cwd, env_map, - /*logs_base_dir*/ None, + logs_base_dir, stdio, use_private_desktop, ) @@ -250,7 +293,11 @@ pub fn spawn_process_with_pipes( return Err(err); } }; - let pi = created.process_info; + let CreatedProcess { + process_info: pi, + _desktop: desktop, + .. + } = created; unsafe { CloseHandle(in_r); @@ -274,6 +321,7 @@ pub fn spawn_process_with_pipes( StderrMode::Separate => Some(err_r), StderrMode::MergeStdout => None, }, + desktop, }) } diff --git a/codex-rs/windows-sandbox-rs/src/sandbox_utils.rs b/codex-rs/windows-sandbox-rs/src/sandbox_utils.rs index ed2aa509cf..5d64e5f844 100644 --- a/codex-rs/windows-sandbox-rs/src/sandbox_utils.rs +++ b/codex-rs/windows-sandbox-rs/src/sandbox_utils.rs @@ -19,16 +19,16 @@ fn find_git_root(start: &Path) -> Option { return Some(cur); } if marker.is_file() { - if let Ok(txt) = std::fs::read_to_string(&marker) { - if let Some(rest) = txt.trim().strip_prefix("gitdir:") { - let gitdir = rest.trim(); - let resolved = if Path::new(gitdir).is_absolute() { - PathBuf::from(gitdir) - } else { - cur.join(gitdir) - }; - return resolved.parent().map(|p| p.to_path_buf()).or(Some(cur)); - } + if let Ok(txt) = std::fs::read_to_string(&marker) + && let Some(rest) = txt.trim().strip_prefix("gitdir:") + { + let gitdir = rest.trim(); + let resolved = if Path::new(gitdir).is_absolute() { + PathBuf::from(gitdir) + } else { + cur.join(gitdir) + }; + return resolved.parent().map(Path::to_path_buf).or(Some(cur)); } return Some(cur); } diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs new file mode 100644 index 0000000000..caf19f332b --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -0,0 +1,306 @@ +use crate::acl::add_allow_ace; +use crate::acl::add_deny_write_ace; +use crate::acl::allow_null_device; +use crate::allow::AllowDenyPaths; +use crate::allow::compute_allow_paths; +use crate::cap::load_or_create_cap_sids; +use crate::cap::workspace_cap_sid_for_cwd; +use crate::env::apply_no_network_to_env; +use crate::env::ensure_non_interactive_pager; +use crate::env::inherit_path_env; +use crate::env::normalize_null_device_env; +use crate::identity::SandboxCreds; +use crate::identity::require_logon_sandbox_creds; +use crate::logging::log_start; +use crate::path_normalization::canonicalize_path; +use crate::policy::SandboxPolicy; +use crate::policy::parse_policy; +use crate::sandbox_utils::ensure_codex_home_exists; +use crate::sandbox_utils::inject_git_safe_directory; +use crate::token::convert_string_sid_to_sid; +use crate::token::create_readonly_token_with_cap; +use crate::token::create_workspace_write_token_with_caps_from; +use crate::token::get_current_token_for_restriction; +use crate::token::get_logon_sid_bytes; +use crate::workspace_acl::is_command_cwd_root; +use crate::workspace_acl::protect_workspace_agents_dir; +use crate::workspace_acl::protect_workspace_codex_dir; +use anyhow::Result; +use std::collections::HashMap; +use std::ffi::c_void; +use std::path::Path; +use std::path::PathBuf; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::Foundation::LocalFree; + +pub(crate) struct SpawnContext { + pub(crate) policy: SandboxPolicy, + pub(crate) current_dir: PathBuf, + pub(crate) sandbox_base: PathBuf, + pub(crate) logs_base_dir: Option, + pub(crate) is_workspace_write: bool, +} + +pub(crate) struct ElevatedSpawnContext { + pub(crate) common: SpawnContext, + pub(crate) sandbox_creds: SandboxCreds, + pub(crate) cap_sids: Vec, +} + +pub(crate) struct LegacySessionSecurity { + pub(crate) h_token: HANDLE, + pub(crate) psid_generic: LocalSid, + pub(crate) psid_workspace: Option, + pub(crate) cap_sid_str: String, +} + +pub(crate) struct LocalSid { + psid: *mut c_void, +} + +impl LocalSid { + pub(crate) fn from_string(sid: &str) -> Result { + let psid = unsafe { convert_string_sid_to_sid(sid) } + .ok_or_else(|| anyhow::anyhow!("invalid SID string: {sid}"))?; + Ok(Self { psid }) + } + + pub(crate) fn as_ptr(&self) -> *mut c_void { + self.psid + } +} + +impl Drop for LocalSid { + fn drop(&mut self) { + if !self.psid.is_null() { + unsafe { + LocalFree(self.psid as HLOCAL); + } + } + } +} + +pub(crate) fn should_apply_network_block(policy: &SandboxPolicy) -> bool { + !policy.has_full_network_access() +} + +pub(crate) fn prepare_legacy_spawn_context( + policy_json_or_preset: &str, + codex_home: &Path, + cwd: &Path, + env_map: &mut HashMap, + command: &[String], + inherit_path: bool, + add_git_safe_directory: bool, +) -> Result { + let policy = parse_policy(policy_json_or_preset)?; + if matches!( + &policy, + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + ) { + anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") + } + + normalize_null_device_env(env_map); + ensure_non_interactive_pager(env_map); + if inherit_path { + inherit_path_env(env_map); + } + if add_git_safe_directory { + inject_git_safe_directory(env_map, cwd); + } + if should_apply_network_block(&policy) { + apply_no_network_to_env(env_map)?; + } + + ensure_codex_home_exists(codex_home)?; + let sandbox_base = codex_home.join(".sandbox"); + std::fs::create_dir_all(&sandbox_base)?; + let logs_base_dir = Some(sandbox_base.clone()); + log_start(command, logs_base_dir.as_deref()); + + let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. }); + + Ok(SpawnContext { + policy, + current_dir: cwd.to_path_buf(), + sandbox_base, + logs_base_dir, + is_workspace_write, + }) +} + +pub(crate) fn prepare_legacy_session_security( + policy: &SandboxPolicy, + codex_home: &Path, + cwd: &Path, +) -> Result { + let caps = load_or_create_cap_sids(codex_home)?; + let (h_token, psid_generic, psid_workspace, cap_sid_str) = unsafe { + match policy { + SandboxPolicy::ReadOnly { .. } => { + let psid = LocalSid::from_string(&caps.readonly)?; + let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?; + (h_token, psid, None, caps.readonly) + } + SandboxPolicy::WorkspaceWrite { .. } => { + let psid_generic = LocalSid::from_string(&caps.workspace)?; + let workspace_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?; + let psid_workspace = LocalSid::from_string(&workspace_sid)?; + let base = get_current_token_for_restriction()?; + let h_token = create_workspace_write_token_with_caps_from( + base, + &[psid_generic.as_ptr(), psid_workspace.as_ptr()], + ); + CloseHandle(base); + let h_token = h_token?; + (h_token, psid_generic, Some(psid_workspace), caps.workspace) + } + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { + unreachable!("dangerous policies rejected before legacy session prep") + } + } + }; + + Ok(LegacySessionSecurity { + h_token, + psid_generic, + psid_workspace, + cap_sid_str, + }) +} + +pub(crate) fn allow_null_device_for_workspace_write(is_workspace_write: bool) { + if !is_workspace_write { + return; + } + + unsafe { + if let Ok(base) = get_current_token_for_restriction() { + if let Ok(bytes) = get_logon_sid_bytes(base) { + let mut tmp = bytes; + let psid = tmp.as_mut_ptr() as *mut c_void; + allow_null_device(psid); + } + CloseHandle(base); + } + } +} + +pub(crate) fn apply_legacy_session_acl_rules( + policy: &SandboxPolicy, + sandbox_policy_cwd: &Path, + current_dir: &Path, + env_map: &HashMap, + psid_generic: &LocalSid, + psid_workspace: Option<&LocalSid>, + persist_aces: bool, +) -> Vec { + let AllowDenyPaths { allow, deny } = + compute_allow_paths(policy, sandbox_policy_cwd, current_dir, env_map); + let mut guards: Vec = Vec::new(); + let canonical_cwd = canonicalize_path(current_dir); + unsafe { + for p in &allow { + let psid = if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) + && is_command_cwd_root(p, &canonical_cwd) + { + psid_workspace.unwrap_or(psid_generic).as_ptr() + } else { + psid_generic.as_ptr() + }; + if matches!(add_allow_ace(p, psid), Ok(true)) && !persist_aces { + guards.push(p.clone()); + } + } + for p in &deny { + if let Ok(added) = add_deny_write_ace(p, psid_generic.as_ptr()) + && added + && !persist_aces + { + guards.push(p.clone()); + } + } + allow_null_device(psid_generic.as_ptr()); + if let Some(psid_workspace) = psid_workspace { + allow_null_device(psid_workspace.as_ptr()); + if persist_aces && matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) { + let _ = protect_workspace_codex_dir(current_dir, psid_workspace.as_ptr()); + let _ = protect_workspace_agents_dir(current_dir, psid_workspace.as_ptr()); + } + } + } + guards +} + +pub(crate) fn prepare_elevated_spawn_context( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + cwd: &Path, + env_map: &mut HashMap, + command: &[String], +) -> Result { + let common = prepare_legacy_spawn_context( + policy_json_or_preset, + codex_home, + cwd, + env_map, + command, + /*inherit_path*/ true, + /*add_git_safe_directory*/ true, + )?; + let AllowDenyPaths { allow, deny } = compute_allow_paths( + &common.policy, + sandbox_policy_cwd, + &common.current_dir, + env_map, + ); + let write_roots: Vec = allow.into_iter().collect(); + let deny_write_paths: Vec = deny.into_iter().collect(); + let write_roots_override = if common.is_workspace_write { + Some(write_roots.as_slice()) + } else { + None + }; + let sandbox_creds = require_logon_sandbox_creds( + &common.policy, + sandbox_policy_cwd, + cwd, + env_map, + codex_home, + /*read_roots_override*/ None, + write_roots_override, + &deny_write_paths, + /*proxy_enforced*/ false, + )?; + let caps = load_or_create_cap_sids(codex_home)?; + let (psid_to_use, cap_sids) = match &common.policy { + SandboxPolicy::ReadOnly { .. } => ( + LocalSid::from_string(&caps.readonly)?, + vec![caps.readonly.clone()], + ), + SandboxPolicy::WorkspaceWrite { .. } => { + let cap_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?; + ( + LocalSid::from_string(&caps.workspace)?, + vec![caps.workspace.clone(), cap_sid], + ) + } + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { + unreachable!("dangerous policies rejected before elevated session prep") + } + }; + + unsafe { + allow_null_device(psid_to_use.as_ptr()); + } + + Ok(ElevatedSpawnContext { + common, + sandbox_creds, + cap_sids, + }) +} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs new file mode 100644 index 0000000000..0ed408fbfe --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs @@ -0,0 +1,118 @@ +use super::windows_common::finish_driver_spawn; +use super::windows_common::make_runner_resizer; +use super::windows_common::start_runner_pipe_writer; +use super::windows_common::start_runner_stdin_writer; +use super::windows_common::start_runner_stdout_reader; +use crate::ipc_framed::EmptyPayload; +use crate::ipc_framed::FramedMessage; +use crate::ipc_framed::Message; +use crate::ipc_framed::SpawnRequest; +use crate::runner_client::spawn_runner_transport; +use crate::spawn_prep::prepare_elevated_spawn_context; +use anyhow::Result; +use codex_utils_pty::ProcessDriver; +use codex_utils_pty::SpawnedProcess; +use std::collections::HashMap; +use std::path::Path; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::sync::oneshot; + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn spawn_windows_sandbox_session_elevated( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + command: Vec, + cwd: &Path, + mut env_map: HashMap, + timeout_ms: Option, + tty: bool, + stdin_open: bool, + use_private_desktop: bool, +) -> Result { + let elevated = prepare_elevated_spawn_context( + policy_json_or_preset, + sandbox_policy_cwd, + codex_home, + cwd, + &mut env_map, + &command, + )?; + + let spawn_request = SpawnRequest { + command: command.clone(), + cwd: cwd.to_path_buf(), + env: env_map.clone(), + policy_json_or_preset: policy_json_or_preset.to_string(), + sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(), + codex_home: elevated.common.sandbox_base.clone(), + real_codex_home: codex_home.to_path_buf(), + cap_sids: elevated.cap_sids.clone(), + timeout_ms, + tty, + stdin_open, + use_private_desktop, + }; + let codex_home = codex_home.to_path_buf(); + let cwd = cwd.to_path_buf(); + let sandbox_creds = elevated.sandbox_creds.clone(); + let logs_base_dir = elevated.common.logs_base_dir.clone(); + let transport = tokio::task::spawn_blocking(move || -> Result<_> { + let mut transport = + spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?; + transport.send_spawn_request(spawn_request)?; + transport.read_spawn_ready()?; + Ok(transport) + }) + .await + .map_err(|err| anyhow::anyhow!("runner handshake task failed: {err}"))??; + let (pipe_write, pipe_read) = transport.into_files(); + + let (writer_tx, writer_rx) = mpsc::channel::>(128); + let (stdout_tx, stdout_rx) = broadcast::channel::>(256); + let stderr_rx = if tty { + None + } else { + Some(broadcast::channel::>(256)) + }; + let (exit_tx, exit_rx) = oneshot::channel::(); + + let outbound_tx = start_runner_pipe_writer(pipe_write); + let writer_handle = start_runner_stdin_writer(writer_rx, outbound_tx.clone(), tty, stdin_open); + let terminator = { + let outbound_tx = outbound_tx.clone(); + Some(Box::new(move || { + let _ = outbound_tx.send(FramedMessage { + version: 1, + message: Message::Terminate { + payload: EmptyPayload::default(), + }, + }); + }) as Box) + }; + + start_runner_stdout_reader( + pipe_read, + stdout_tx, + stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()), + exit_tx, + ); + + Ok(finish_driver_spawn( + ProcessDriver { + writer_tx, + stdout_rx, + stderr_rx: stderr_rx.map(|(_tx, rx)| rx), + exit_rx, + terminator, + writer_handle: Some(writer_handle), + resizer: if tty { + Some(make_runner_resizer(outbound_tx)) + } else { + None + }, + }, + stdin_open, + )) +} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs new file mode 100644 index 0000000000..ba1f15a3be --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs @@ -0,0 +1,439 @@ +use super::windows_common::finish_driver_spawn; +use super::windows_common::normalize_windows_tty_input; +use crate::acl::revoke_ace; +use crate::conpty::spawn_conpty_process_as_user; +use crate::desktop::LaunchDesktop; +use crate::logging::log_failure; +use crate::logging::log_success; +use crate::process::StderrMode; +use crate::process::StdinMode; +use crate::process::read_handle_loop; +use crate::process::spawn_process_with_pipes; +use crate::spawn_prep::LocalSid; +use crate::spawn_prep::allow_null_device_for_workspace_write; +use crate::spawn_prep::apply_legacy_session_acl_rules; +use crate::spawn_prep::prepare_legacy_session_security; +use crate::spawn_prep::prepare_legacy_spawn_context; +use anyhow::Result; +use codex_utils_pty::ProcessDriver; +use codex_utils_pty::SpawnedProcess; +use codex_utils_pty::TerminalSize; +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; +use std::ptr; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; +use windows_sys::Win32::Storage::FileSystem::WriteFile; +use windows_sys::Win32::System::Console::COORD; +use windows_sys::Win32::System::Console::ClosePseudoConsole; +use windows_sys::Win32::System::Console::ResizePseudoConsole; +use windows_sys::Win32::System::Threading::GetExitCodeProcess; +use windows_sys::Win32::System::Threading::INFINITE; +use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; +use windows_sys::Win32::System::Threading::TerminateProcess; +use windows_sys::Win32::System::Threading::WaitForSingleObject; + +const WAIT_TIMEOUT: u32 = 0x0000_0102; + +struct LegacyProcessHandles { + process: PROCESS_INFORMATION, + output_join: std::thread::JoinHandle<()>, + writer_handle: tokio::task::JoinHandle<()>, + hpc: Option, + token_handle: HANDLE, + desktop: Option, +} + +#[allow(clippy::too_many_arguments)] +fn spawn_legacy_process( + h_token: HANDLE, + command: &[String], + cwd: &Path, + env_map: &HashMap, + use_private_desktop: bool, + tty: bool, + stdin_open: bool, + stdout_tx: broadcast::Sender>, + stderr_tx: Option>>, + writer_rx: mpsc::Receiver>, + logs_base_dir: Option<&Path>, +) -> Result { + let (pi, output_join, writer_handle, hpc, desktop) = if tty { + let (pi, conpty) = spawn_conpty_process_as_user( + h_token, + command, + cwd, + env_map, + use_private_desktop, + logs_base_dir, + )?; + let (hpc, input_write, output_read, desktop) = conpty.into_raw(); + let output_join = spawn_output_reader(output_read, stdout_tx); + let writer_handle = spawn_input_writer( + Some(input_write), + writer_rx, + /*normalize_newlines*/ true, + ); + (pi, output_join, writer_handle, Some(hpc), desktop) + } else { + let pipe_handles = spawn_process_with_pipes( + h_token, + command, + cwd, + env_map, + if stdin_open { + StdinMode::Open + } else { + StdinMode::Closed + }, + StderrMode::Separate, + use_private_desktop, + logs_base_dir, + )?; + let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx); + let Some(stderr_read) = pipe_handles.stderr_read else { + anyhow::bail!("separate stderr handle should be present"); + }; + let Some(stderr_tx) = stderr_tx else { + anyhow::bail!("separate stderr channel should be present"); + }; + let stderr_join = spawn_output_reader(stderr_read, stderr_tx); + let output_join = std::thread::spawn(move || { + let _ = stdout_join.join(); + let _ = stderr_join.join(); + }); + let writer_handle = spawn_input_writer( + pipe_handles.stdin_write, + writer_rx, + /*normalize_newlines*/ false, + ); + ( + pipe_handles.process, + output_join, + writer_handle, + None, + Some(pipe_handles.desktop), + ) + }; + Ok(LegacyProcessHandles { + process: pi, + output_join, + writer_handle, + hpc, + token_handle: h_token, + desktop, + }) +} + +fn spawn_output_reader( + output_read: HANDLE, + output_tx: broadcast::Sender>, +) -> std::thread::JoinHandle<()> { + read_handle_loop(output_read, move |chunk| { + let _ = output_tx.send(chunk.to_vec()); + }) +} + +fn spawn_input_writer( + input_write: Option, + mut writer_rx: mpsc::Receiver>, + normalize_newlines: bool, +) -> tokio::task::JoinHandle<()> { + tokio::task::spawn_blocking(move || { + let mut previous_was_cr = false; + while let Some(bytes) = writer_rx.blocking_recv() { + let Some(handle) = input_write else { + continue; + }; + let bytes = if normalize_newlines { + normalize_windows_tty_input(&bytes, &mut previous_was_cr) + } else { + bytes + }; + if write_all_handle(handle, &bytes).is_err() { + break; + } + } + if let Some(handle) = input_write { + unsafe { + CloseHandle(handle); + } + } + }) +} + +fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> { + while !bytes.is_empty() { + let mut written = 0u32; + let ok = unsafe { + WriteFile( + handle, + bytes.as_ptr() as *const _, + bytes.len() as u32, + &mut written, + ptr::null_mut(), + ) + }; + if ok == 0 { + let err = unsafe { GetLastError() } as i32; + return Err(anyhow::anyhow!("WriteFile failed: {err}")); + } + if written == 0 { + anyhow::bail!("WriteFile returned success but wrote 0 bytes"); + } + bytes = &bytes[written as usize..]; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn finalize_exit( + exit_tx: oneshot::Sender, + process_handle: Arc>>, + thread_handle: HANDLE, + output_join: std::thread::JoinHandle<()>, + guards: Vec, + cap_sid: Option, + logs_base_dir: Option<&Path>, + command: Vec, +) { + let exit_code = { + let mut raw_exit = 1u32; + if let Ok(guard) = process_handle.lock() + && let Some(handle) = guard.as_ref() + { + unsafe { + WaitForSingleObject(*handle, INFINITE); + GetExitCodeProcess(*handle, &mut raw_exit); + } + } + raw_exit as i32 + }; + + let _ = output_join.join(); + let _ = exit_tx.send(exit_code); + + unsafe { + if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE { + CloseHandle(thread_handle); + } + if let Ok(mut guard) = process_handle.lock() + && let Some(handle) = guard.take() + { + CloseHandle(handle); + } + } + + if exit_code == 0 { + log_success(&command, logs_base_dir); + } else { + log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir); + } + + if let Some(cap_sid) = cap_sid + && let Ok(sid) = LocalSid::from_string(&cap_sid) + { + unsafe { + for path in guards { + revoke_ace(&path, sid.as_ptr()); + } + } + } +} + +fn resize_conpty_handle(hpc: &Arc>>, size: TerminalSize) -> Result<()> { + let guard = hpc + .lock() + .map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?; + let hpc = guard + .as_ref() + .copied() + .ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?; + let result = unsafe { + ResizePseudoConsole( + hpc, + COORD { + X: size.cols as i16, + Y: size.rows as i16, + }, + ) + }; + if result == 0 { + Ok(()) + } else { + Err(anyhow::anyhow!( + "failed to resize console: HRESULT {result}" + )) + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn spawn_windows_sandbox_session_legacy( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + command: Vec, + cwd: &Path, + mut env_map: HashMap, + timeout_ms: Option, + tty: bool, + stdin_open: bool, + use_private_desktop: bool, +) -> Result { + let common = prepare_legacy_spawn_context( + policy_json_or_preset, + codex_home, + cwd, + &mut env_map, + &command, + /*inherit_path*/ false, + /*add_git_safe_directory*/ false, + )?; + if !common.policy.has_full_disk_read_access() { + anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend"); + } + let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?; + allow_null_device_for_workspace_write(common.is_workspace_write); + + let persist_aces = common.is_workspace_write; + let guards = apply_legacy_session_acl_rules( + &common.policy, + sandbox_policy_cwd, + &common.current_dir, + &env_map, + &security.psid_generic, + security.psid_workspace.as_ref(), + persist_aces, + ); + + let (writer_tx, writer_rx) = mpsc::channel::>(128); + let (stdout_tx, stdout_rx) = broadcast::channel::>(256); + let stderr_rx = if tty { + None + } else { + Some(broadcast::channel::>(256)) + }; + let (exit_tx, exit_rx) = oneshot::channel::(); + + let LegacyProcessHandles { + process: pi, + output_join, + writer_handle, + hpc, + token_handle, + desktop, + } = match spawn_legacy_process( + security.h_token, + &command, + cwd, + &env_map, + use_private_desktop, + tty, + stdin_open, + stdout_tx, + stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()), + writer_rx, + common.logs_base_dir.as_deref(), + ) { + Ok(handles) => handles, + Err(err) => { + unsafe { + if !persist_aces + && !guards.is_empty() + && let Ok(sid) = LocalSid::from_string(&security.cap_sid_str) + { + for path in &guards { + revoke_ace(path, sid.as_ptr()); + } + } + CloseHandle(security.h_token); + } + return Err(err); + } + }; + let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc)))); + + let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess))); + let wait_handle = Arc::clone(&process_handle); + let command_for_wait = command.clone(); + let guards_for_wait = if persist_aces { Vec::new() } else { guards }; + let cap_sid_for_wait = if guards_for_wait.is_empty() { + None + } else { + Some(security.cap_sid_str) + }; + let hpc_for_wait = hpc_handle.clone(); + std::thread::spawn(move || { + let _desktop = desktop; + let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE); + let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) }; + if wait_res == WAIT_TIMEOUT { + unsafe { + if let Ok(guard) = wait_handle.lock() + && let Some(handle) = guard.as_ref() + { + let _ = TerminateProcess(*handle, 1); + } + } + } + if let Some(hpc) = hpc_for_wait + && let Ok(mut guard) = hpc.lock() + && let Some(hpc) = guard.take() + { + unsafe { + ClosePseudoConsole(hpc); + } + } + unsafe { + if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE { + CloseHandle(token_handle); + } + } + finalize_exit( + exit_tx, + wait_handle, + pi.hThread, + output_join, + guards_for_wait, + cap_sid_for_wait, + common.logs_base_dir.as_deref(), + command_for_wait, + ); + }); + + let terminator = { + let process_handle = Arc::clone(&process_handle); + Some(Box::new(move || { + if let Ok(guard) = process_handle.lock() + && let Some(handle) = guard.as_ref() + { + unsafe { + let _ = TerminateProcess(*handle, 1); + } + } + }) as Box) + }; + + let driver = ProcessDriver { + writer_tx, + stdout_rx, + stderr_rx: stderr_rx.map(|(_tx, rx)| rx), + exit_rx, + terminator, + writer_handle: Some(writer_handle), + resizer: hpc_handle.map(|hpc| { + Box::new(move |size| resize_conpty_handle(&hpc, size)) + as Box Result<()> + Send> + }), + }; + + Ok(finish_driver_spawn(driver, stdin_open)) +} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/mod.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/mod.rs new file mode 100644 index 0000000000..9d2aaa9070 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod elevated; +pub(crate) mod legacy; +pub(crate) mod windows_common; diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/windows_common.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/windows_common.rs new file mode 100644 index 0000000000..3396b4ed98 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/windows_common.rs @@ -0,0 +1,191 @@ +use crate::ipc_framed::EmptyPayload; +use crate::ipc_framed::FramedMessage; +use crate::ipc_framed::Message; +use crate::ipc_framed::OutputStream; +use crate::ipc_framed::ResizePayload; +use crate::ipc_framed::StdinPayload; +use crate::ipc_framed::decode_bytes; +use crate::ipc_framed::encode_bytes; +use anyhow::Result; +use codex_utils_pty::ProcessDriver; +use codex_utils_pty::SpawnedProcess; +use codex_utils_pty::TerminalSize; +use codex_utils_pty::spawn_from_driver; +use std::fs::File; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::sync::oneshot; + +pub(crate) fn finish_driver_spawn(driver: ProcessDriver, stdin_open: bool) -> SpawnedProcess { + let spawned = spawn_from_driver(driver); + if !stdin_open { + spawned.session.close_stdin(); + } + spawned +} + +pub(crate) fn normalize_windows_tty_input(bytes: &[u8], previous_was_cr: &mut bool) -> Vec { + let mut normalized = Vec::with_capacity(bytes.len()); + for &byte in bytes { + if byte == b'\n' { + if !*previous_was_cr { + normalized.push(b'\r'); + } + normalized.push(b'\n'); + *previous_was_cr = false; + } else { + normalized.push(byte); + *previous_was_cr = byte == b'\r'; + } + } + normalized +} + +pub(crate) fn start_runner_pipe_writer( + mut pipe_write: File, +) -> std::sync::mpsc::Sender { + let (outbound_tx, outbound_rx) = std::sync::mpsc::channel::(); + tokio::task::spawn_blocking(move || { + while let Ok(msg) = outbound_rx.recv() { + if crate::ipc_framed::write_frame(&mut pipe_write, &msg).is_err() { + break; + } + } + }); + outbound_tx +} + +pub(crate) fn start_runner_stdin_writer( + mut writer_rx: mpsc::Receiver>, + outbound_tx: std::sync::mpsc::Sender, + normalize_newlines: bool, + stdin_open: bool, +) -> tokio::task::JoinHandle<()> { + tokio::task::spawn_blocking(move || { + let mut previous_was_cr = false; + while let Some(bytes) = writer_rx.blocking_recv() { + let bytes = if normalize_newlines { + normalize_windows_tty_input(&bytes, &mut previous_was_cr) + } else { + bytes + }; + let msg = FramedMessage { + version: 1, + message: Message::Stdin { + payload: StdinPayload { + data_b64: encode_bytes(&bytes), + }, + }, + }; + if outbound_tx.send(msg).is_err() { + break; + } + } + if stdin_open { + let _ = outbound_tx.send(FramedMessage { + version: 1, + message: Message::CloseStdin { + payload: EmptyPayload::default(), + }, + }); + } + }) +} + +pub(crate) fn start_runner_stdout_reader( + mut pipe_read: File, + stdout_tx: broadcast::Sender>, + stderr_tx: Option>>, + exit_tx: oneshot::Sender, +) { + std::thread::spawn(move || { + loop { + let msg = match crate::ipc_framed::read_frame(&mut pipe_read) { + Ok(Some(v)) => v, + Ok(None) => { + send_runner_error( + "runner pipe closed before exit", + &stdout_tx, + stderr_tx.as_ref(), + ); + let _ = exit_tx.send(-1); + break; + } + Err(err) => { + send_runner_error( + &format!("runner read failed: {err}"), + &stdout_tx, + stderr_tx.as_ref(), + ); + let _ = exit_tx.send(-1); + break; + } + }; + + match msg.message { + Message::Output { payload } => { + if let Ok(data) = decode_bytes(&payload.data_b64) { + match payload.stream { + OutputStream::Stdout => { + let _ = stdout_tx.send(data); + } + OutputStream::Stderr => { + if let Some(stderr_tx) = stderr_tx.as_ref() { + let _ = stderr_tx.send(data); + } else { + let _ = stdout_tx.send(data); + } + } + } + } + } + Message::Exit { payload } => { + let _ = exit_tx.send(payload.exit_code); + break; + } + Message::Error { payload } => { + send_runner_error(&payload.message, &stdout_tx, stderr_tx.as_ref()); + let _ = exit_tx.send(-1); + break; + } + Message::SpawnReady { .. } + | Message::Stdin { .. } + | Message::CloseStdin { .. } + | Message::Resize { .. } + | Message::SpawnRequest { .. } + | Message::Terminate { .. } => {} + } + } + }); +} + +pub(crate) fn make_runner_resizer( + outbound_tx: std::sync::mpsc::Sender, +) -> Box Result<()> + Send> { + Box::new(move |size: TerminalSize| { + outbound_tx + .send(FramedMessage { + version: 1, + message: Message::Resize { + payload: ResizePayload { + rows: size.rows, + cols: size.cols, + }, + }, + }) + .map_err(|_| anyhow::anyhow!("runner resize pipe closed")) + }) +} + +fn send_runner_error( + message: &str, + stdout_tx: &broadcast::Sender>, + stderr_tx: Option<&broadcast::Sender>>, +) { + let formatted = format!("runner error: {message}\n").into_bytes(); + if let Some(stderr_tx) = stderr_tx { + let _ = stderr_tx.send(formatted); + } else { + let _ = stdout_tx.send(formatted); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/session.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/session.rs new file mode 100644 index 0000000000..ced69433d1 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/session.rs @@ -0,0 +1,83 @@ +//! Unified exec session spawner for Windows sandboxing. +//! +//! This module is the thin orchestration layer for Windows unified-exec sessions. +//! Backend-specific mechanics live in sibling modules: +//! - `backends::legacy` adapts the direct restricted-token spawn path into a live session. +//! - `backends::elevated` adapts the elevated command-runner IPC path into the same session API. +//! - `backends::windows_common` holds the small shared Windows backend helpers +//! used by both. + +mod backends; + +use anyhow::Result; +use codex_utils_pty::SpawnedProcess; +use std::collections::HashMap; +use std::path::Path; + +#[allow(clippy::too_many_arguments)] +pub async fn spawn_windows_sandbox_session_legacy( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + command: Vec, + cwd: &Path, + env_map: HashMap, + timeout_ms: Option, + tty: bool, + stdin_open: bool, + use_private_desktop: bool, +) -> Result { + backends::legacy::spawn_windows_sandbox_session_legacy( + policy_json_or_preset, + sandbox_policy_cwd, + codex_home, + command, + cwd, + env_map, + timeout_ms, + tty, + stdin_open, + use_private_desktop, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +pub async fn spawn_windows_sandbox_session_elevated( + policy_json_or_preset: &str, + sandbox_policy_cwd: &Path, + codex_home: &Path, + command: Vec, + cwd: &Path, + env_map: HashMap, + timeout_ms: Option, + tty: bool, + stdin_open: bool, + use_private_desktop: bool, +) -> Result { + backends::elevated::spawn_windows_sandbox_session_elevated( + policy_json_or_preset, + sandbox_policy_cwd, + codex_home, + command, + cwd, + env_map, + timeout_ms, + tty, + stdin_open, + use_private_desktop, + ) + .await +} + +#[cfg(test)] +pub(crate) use backends::windows_common::finish_driver_spawn; +#[cfg(test)] +pub(crate) use backends::windows_common::make_runner_resizer; +#[cfg(test)] +pub(crate) use backends::windows_common::start_runner_pipe_writer; +#[cfg(test)] +pub(crate) use backends::windows_common::start_runner_stdin_writer; + +#[cfg(test)] +mod tests; diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs new file mode 100644 index 0000000000..d9de8104aa --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs @@ -0,0 +1,533 @@ +#![cfg(target_os = "windows")] + +use super::spawn_windows_sandbox_session_legacy; +use crate::ipc_framed::Message; +use crate::ipc_framed::decode_bytes; +use crate::ipc_framed::read_frame; +use crate::run_windows_sandbox_capture; +use codex_utils_pty::ProcessDriver; +use pretty_assertions::assert_eq; +use std::collections::HashMap; +use std::fs; +use std::fs::OpenOptions; +use std::io::Seek; +use std::io::SeekFrom; +use std::path::Path; +use std::path::PathBuf; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::Duration; +use std::time::Instant; +use tempfile::TempDir; +use tokio::runtime::Builder; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio::time::timeout; + +static TEST_HOME_COUNTER: AtomicU64 = AtomicU64::new(0); + +fn current_thread_runtime() -> tokio::runtime::Runtime { + Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime") +} + +fn pwsh_path() -> Option { + let program_files = std::env::var_os("ProgramFiles")?; + let path = PathBuf::from(program_files).join("PowerShell\\7\\pwsh.exe"); + path.is_file().then_some(path) +} + +fn sandbox_cwd() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("repo root") + .to_path_buf() +} + +fn sandbox_home(name: &str) -> TempDir { + let id = TEST_HOME_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("codex-windows-sandbox-{name}-{id}")); + let _ = fs::remove_dir_all(&path); + fs::create_dir_all(&path).expect("create sandbox home"); + tempfile::TempDir::new_in(&path).expect("create sandbox home tempdir") +} + +fn sandbox_log(codex_home: &Path) -> String { + let log_path = codex_home.join(".sandbox").join("sandbox.log"); + fs::read_to_string(&log_path) + .unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display())) +} + +fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec { + let deadline = Instant::now() + Duration::from_secs(2); + loop { + let mut reader = OpenOptions::new() + .read(true) + .open(frames_path) + .expect("open frame file for read"); + reader + .seek(SeekFrom::Start(0)) + .expect("seek to start of frame file"); + + let mut frames = Vec::new(); + loop { + match read_frame(&mut reader) { + Ok(Some(frame)) => frames.push(frame.message), + Ok(None) => break, + Err(_) => break, + } + } + + if frames.len() >= expected_frames { + return frames; + } + assert!( + Instant::now() < deadline, + "timed out waiting for {expected_frames} frames, saw {}", + frames.len() + ); + std::thread::sleep(Duration::from_millis(10)); + } +} + +async fn collect_stdout_and_exit( + spawned: codex_utils_pty::SpawnedProcess, + codex_home: &Path, + timeout_duration: Duration, +) -> (Vec, i32) { + let codex_utils_pty::SpawnedProcess { + session: _session, + mut stdout_rx, + stderr_rx: _stderr_rx, + exit_rx, + } = spawned; + let stdout_task = tokio::spawn(async move { + let mut stdout = Vec::new(); + while let Some(chunk) = stdout_rx.recv().await { + stdout.extend(chunk); + } + stdout + }); + let exit_code = timeout(timeout_duration, exit_rx) + .await + .unwrap_or_else(|_| panic!("timed out waiting for exit\n{}", sandbox_log(codex_home))) + .unwrap_or(-1); + let stdout = timeout(timeout_duration, stdout_task) + .await + .unwrap_or_else(|_| { + panic!( + "timed out waiting for stdout task\n{}", + sandbox_log(codex_home) + ) + }) + .expect("stdout task join"); + (stdout, exit_code) +} + +#[test] +fn legacy_non_tty_cmd_emits_output() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-non-tty-cmd"); + println!("cmd codex_home={}", codex_home.path().display()); + let spawned = spawn_windows_sandbox_session_legacy( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + "C:\\Windows\\System32\\cmd.exe".to_string(), + "/c".to_string(), + "echo LEGACY-NONTTY-CMD".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(5_000), + /*tty*/ false, + /*stdin_open*/ false, + /*use_private_desktop*/ true, + ) + .await + .expect("spawn legacy non-tty cmd session"); + println!("cmd spawn returned"); + let (stdout, exit_code) = + collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await; + println!("cmd collect returned exit_code={exit_code}"); + let stdout = String::from_utf8_lossy(&stdout); + assert_eq!(exit_code, 0, "stdout={stdout:?}"); + assert!(stdout.contains("LEGACY-NONTTY-CMD"), "stdout={stdout:?}"); + }); +} + +#[test] +fn legacy_non_tty_powershell_emits_output() { + let Some(pwsh) = pwsh_path() else { + return; + }; + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-non-tty-pwsh"); + println!("pwsh codex_home={}", codex_home.path().display()); + let spawned = spawn_windows_sandbox_session_legacy( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + pwsh.display().to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Write-Output LEGACY-NONTTY-DIRECT".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(5_000), + /*tty*/ false, + /*stdin_open*/ false, + /*use_private_desktop*/ true, + ) + .await + .expect("spawn legacy non-tty powershell session"); + println!("pwsh spawn returned"); + let (stdout, exit_code) = + collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await; + println!("pwsh collect returned exit_code={exit_code}"); + let stdout = String::from_utf8_lossy(&stdout); + assert_eq!(exit_code, 0, "stdout={stdout:?}"); + assert!(stdout.contains("LEGACY-NONTTY-DIRECT"), "stdout={stdout:?}"); + }); +} + +#[test] +fn finish_driver_spawn_keeps_stdin_open_when_requested() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let (writer_tx, mut writer_rx) = mpsc::channel::>(1); + let (_stdout_tx, stdout_rx) = broadcast::channel::>(1); + let (exit_tx, exit_rx) = oneshot::channel::(); + drop(exit_tx); + + let spawned = super::finish_driver_spawn( + ProcessDriver { + writer_tx, + stdout_rx, + stderr_rx: None, + exit_rx, + terminator: None, + writer_handle: None, + resizer: None, + }, + /*stdin_open*/ true, + ); + + spawned + .session + .writer_sender() + .send(b"open".to_vec()) + .await + .expect("stdin should stay open"); + assert_eq!(writer_rx.recv().await, Some(b"open".to_vec())); + }); +} + +#[test] +fn finish_driver_spawn_closes_stdin_when_not_requested() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let (writer_tx, _writer_rx) = mpsc::channel::>(1); + let (_stdout_tx, stdout_rx) = broadcast::channel::>(1); + let (exit_tx, exit_rx) = oneshot::channel::(); + drop(exit_tx); + + let spawned = super::finish_driver_spawn( + ProcessDriver { + writer_tx, + stdout_rx, + stderr_rx: None, + exit_rx, + terminator: None, + writer_handle: None, + resizer: None, + }, + /*stdin_open*/ false, + ); + + assert!( + spawned + .session + .writer_sender() + .send(b"closed".to_vec()) + .await + .is_err(), + "stdin should be closed when streaming input is disabled" + ); + }); +} + +#[test] +fn runner_stdin_writer_sends_close_stdin_after_input_eof() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let tempdir = TempDir::new().expect("create tempdir"); + let frames_path = tempdir.path().join("runner-stdin-frames.bin"); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&frames_path) + .expect("create frame file"); + let outbound_tx = super::start_runner_pipe_writer(file); + let (writer_tx, writer_rx) = mpsc::channel::>(1); + let writer_handle = super::start_runner_stdin_writer( + writer_rx, + outbound_tx, + /*normalize_newlines*/ false, + /*stdin_open*/ true, + ); + + writer_tx + .send(b"hello".to_vec()) + .await + .expect("send stdin bytes"); + drop(writer_tx); + writer_handle.await.expect("join stdin writer"); + + let frames = wait_for_frame_count(&frames_path, 2); + + match &frames[0] { + Message::Stdin { payload } => { + let bytes = decode_bytes(&payload.data_b64).expect("decode stdin payload"); + assert_eq!(bytes, b"hello".to_vec()); + } + other => panic!("expected stdin frame, got {other:?}"), + } + + match &frames[1] { + Message::CloseStdin { .. } => {} + other => panic!("expected close-stdin frame, got {other:?}"), + } + }); +} + +#[test] +fn runner_resizer_sends_resize_frame() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let tempdir = TempDir::new().expect("create tempdir"); + let frames_path = tempdir.path().join("runner-resize-frames.bin"); + let file = OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&frames_path) + .expect("create frame file"); + let outbound_tx = super::start_runner_pipe_writer(file); + let mut resizer = super::make_runner_resizer(outbound_tx); + + resizer(codex_utils_pty::TerminalSize { + rows: 45, + cols: 132, + }) + .expect("send resize frame"); + + let frames = wait_for_frame_count(&frames_path, 1); + match &frames[0] { + Message::Resize { payload } => { + assert_eq!(payload.rows, 45); + assert_eq!(payload.cols, 132); + } + other => panic!("expected resize frame, got {other:?}"), + } + }); +} + +#[test] +fn legacy_capture_powershell_emits_output() { + let Some(pwsh) = pwsh_path() else { + return; + }; + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-capture-pwsh"); + println!("capture pwsh codex_home={}", codex_home.path().display()); + let result = run_windows_sandbox_capture( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + pwsh.display().to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "Write-Output LEGACY-CAPTURE-DIRECT".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(10_000), + /*use_private_desktop*/ true, + ) + .expect("run legacy capture powershell"); + println!("capture pwsh exit_code={}", result.exit_code); + println!("capture pwsh timed_out={}", result.timed_out); + let stdout = String::from_utf8_lossy(&result.stdout); + let stderr = String::from_utf8_lossy(&result.stderr); + println!("capture pwsh stderr={stderr:?}"); + assert_eq!(result.exit_code, 0, "stdout={stdout:?} stderr={stderr:?}"); + assert!( + stdout.contains("LEGACY-CAPTURE-DIRECT"), + "stdout={stdout:?}" + ); +} + +#[test] +fn legacy_tty_powershell_emits_output_and_accepts_input() { + let Some(pwsh) = pwsh_path() else { + return; + }; + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-tty-pwsh"); + println!("tty pwsh codex_home={}", codex_home.path().display()); + let spawned = spawn_windows_sandbox_session_legacy( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + pwsh.display().to_string(), + "-NoLogo".to_string(), + "-NoProfile".to_string(), + "-NoExit".to_string(), + "-Command".to_string(), + "$PID; Write-Output ready".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(10_000), + /*tty*/ true, + /*stdin_open*/ true, + /*use_private_desktop*/ true, + ) + .await + .expect("spawn legacy tty powershell session"); + println!("tty pwsh spawn returned"); + + let writer = spawned.session.writer_sender(); + writer + .send(b"Write-Output second\n".to_vec()) + .await + .expect("send second command"); + writer + .send(b"exit\n".to_vec()) + .await + .expect("send exit command"); + spawned.session.close_stdin(); + + let (stdout, exit_code) = + collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await; + let stdout = String::from_utf8_lossy(&stdout); + assert_eq!(exit_code, 0, "stdout={stdout:?}"); + assert!(stdout.contains("ready"), "stdout={stdout:?}"); + assert!(stdout.contains("second"), "stdout={stdout:?}"); + }); +} + +#[test] +fn legacy_tty_cmd_emits_output_and_accepts_input() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-tty-cmd"); + println!("tty cmd codex_home={}", codex_home.path().display()); + let spawned = spawn_windows_sandbox_session_legacy( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + "C:\\Windows\\System32\\cmd.exe".to_string(), + "/K".to_string(), + "echo ready".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(10_000), + /*tty*/ true, + /*stdin_open*/ true, + /*use_private_desktop*/ true, + ) + .await + .expect("spawn legacy tty cmd session"); + println!("tty cmd spawn returned"); + + let writer = spawned.session.writer_sender(); + writer + .send(b"echo second\n".to_vec()) + .await + .expect("send second command"); + writer + .send(b"exit\n".to_vec()) + .await + .expect("send exit command"); + spawned.session.close_stdin(); + + let (stdout, exit_code) = + collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await; + let stdout = String::from_utf8_lossy(&stdout); + assert_eq!(exit_code, 0, "stdout={stdout:?}"); + assert!(stdout.contains("ready"), "stdout={stdout:?}"); + assert!(stdout.contains("second"), "stdout={stdout:?}"); + }); +} + +#[test] +fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() { + let runtime = current_thread_runtime(); + runtime.block_on(async move { + let cwd = sandbox_cwd(); + let codex_home = sandbox_home("legacy-tty-cmd-default-desktop"); + println!( + "tty cmd default desktop codex_home={}", + codex_home.path().display() + ); + let spawned = spawn_windows_sandbox_session_legacy( + "workspace-write", + cwd.as_path(), + codex_home.path(), + vec![ + "C:\\Windows\\System32\\cmd.exe".to_string(), + "/K".to_string(), + "echo ready".to_string(), + ], + cwd.as_path(), + HashMap::new(), + Some(10_000), + /*tty*/ true, + /*stdin_open*/ true, + /*use_private_desktop*/ false, + ) + .await + .expect("spawn legacy tty cmd session"); + println!("tty cmd default desktop spawn returned"); + + let writer = spawned.session.writer_sender(); + writer + .send(b"echo second\n".to_vec()) + .await + .expect("send second command"); + writer + .send(b"exit\n".to_vec()) + .await + .expect("send exit command"); + spawned.session.close_stdin(); + + let (stdout, exit_code) = + collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await; + let stdout = String::from_utf8_lossy(&stdout); + assert_eq!(exit_code, 0, "stdout={stdout:?}"); + assert!(stdout.contains("ready"), "stdout={stdout:?}"); + assert!(stdout.contains("second"), "stdout={stdout:?}"); + }); +} diff --git a/codex-rs/windows-sandbox-rs/src/winutil.rs b/codex-rs/windows-sandbox-rs/src/winutil.rs index 1486c7f857..f014e06072 100644 --- a/codex-rs/windows-sandbox-rs/src/winutil.rs +++ b/codex-rs/windows-sandbox-rs/src/winutil.rs @@ -64,6 +64,15 @@ pub fn quote_windows_arg(arg: &str) -> String { quoted } +/// Build a Windows command line for CreateProcess-style APIs. +#[cfg(target_os = "windows")] +pub fn argv_to_command_line(argv: &[String]) -> String { + argv.iter() + .map(|arg| quote_windows_arg(arg)) + .collect::>() + .join(" ") +} + // Produce a readable description for a Win32 error code. pub fn format_last_error(err: i32) -> String { unsafe { @@ -190,3 +199,38 @@ fn sid_bytes_from_string(sid_str: &str) -> Result> { } Ok(out) } + +#[cfg(test)] +mod tests { + use super::argv_to_command_line; + use pretty_assertions::assert_eq; + + #[test] + fn argv_to_command_line_quotes_each_argument_independently() { + let argv = vec![ + "cmd.exe".to_string(), + "/c".to_string(), + "\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\" -NoProfile -EncodedCommand abc==" + .to_string(), + ]; + + assert_eq!( + argv_to_command_line(&argv), + "cmd.exe /c \"\\\"C:\\Program Files\\PowerShell\\7\\pwsh.exe\\\" -NoProfile -EncodedCommand abc==\"" + ); + } + + #[test] + fn argv_to_command_line_quotes_regular_program_args() { + let argv = vec![ + "pwsh.exe".to_string(), + "-Command".to_string(), + "Write-Output \"hello world\"".to_string(), + ]; + + assert_eq!( + argv_to_command_line(&argv), + "pwsh.exe -Command \"Write-Output \\\"hello world\\\"\"" + ); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/workspace_acl.rs b/codex-rs/windows-sandbox-rs/src/workspace_acl.rs index d011db30b5..7143212b54 100644 --- a/codex-rs/windows-sandbox-rs/src/workspace_acl.rs +++ b/codex-rs/windows-sandbox-rs/src/workspace_acl.rs @@ -1,6 +1,30 @@ +use crate::acl::add_deny_write_ace; use crate::path_normalization::canonicalize_path; +use anyhow::Result; +use std::ffi::c_void; use std::path::Path; pub fn is_command_cwd_root(root: &Path, canonical_command_cwd: &Path) -> bool { canonicalize_path(root) == canonical_command_cwd } + +/// # Safety +/// Caller must ensure `psid` is a valid SID pointer. +pub unsafe fn protect_workspace_codex_dir(cwd: &Path, psid: *mut c_void) -> Result { + protect_workspace_subdir(cwd, psid, ".codex") +} + +/// # Safety +/// Caller must ensure `psid` is a valid SID pointer. +pub unsafe fn protect_workspace_agents_dir(cwd: &Path, psid: *mut c_void) -> Result { + protect_workspace_subdir(cwd, psid, ".agents") +} + +unsafe fn protect_workspace_subdir(cwd: &Path, psid: *mut c_void, subdir: &str) -> Result { + let path = cwd.join(subdir); + if path.is_dir() { + add_deny_write_ace(&path, psid) + } else { + Ok(false) + } +} diff --git a/sdk/python/src/codex_app_server/api.py b/sdk/python/src/codex_app_server/api.py index 5009d9bbf5..c330e3f743 100644 --- a/sdk/python/src/codex_app_server/api.py +++ b/sdk/python/src/codex_app_server/api.py @@ -105,7 +105,11 @@ class Codex: normalized_server_name = (server_name or "").strip() normalized_server_version = (server_version or "").strip() - if not user_agent or not normalized_server_name or not normalized_server_version: + if ( + not user_agent + or not normalized_server_name + or not normalized_server_version + ): raise RuntimeError( "initialize response missing required metadata " f"(user_agent={user_agent!r}, server_name={normalized_server_name!r}, server_version={normalized_server_version!r})" @@ -146,6 +150,7 @@ class Codex: sandbox: SandboxMode | None = None, service_name: str | None = None, service_tier: ServiceTier | None = None, + session_start_source: ThreadStartSource | None = None, ) -> Thread: params = ThreadStartParams( approval_policy=approval_policy, @@ -161,6 +166,7 @@ class Codex: sandbox=sandbox, service_name=service_name, service_tier=service_tier, + session_start_source=session_start_source, ) started = self._client.thread_start(params) return Thread(self._client, started.thread.id) @@ -174,6 +180,7 @@ class Codex: limit: int | None = None, model_providers: list[str] | None = None, search_term: str | None = None, + sort_direction: SortDirection | None = None, sort_key: ThreadSortKey | None = None, source_kinds: list[ThreadSourceKind] | None = None, ) -> ThreadListResponse: @@ -184,6 +191,7 @@ class Codex: limit=limit, model_providers=model_providers, search_term=search_term, + sort_direction=sort_direction, sort_key=sort_key, source_kinds=source_kinds, ) @@ -336,6 +344,7 @@ class AsyncCodex: sandbox: SandboxMode | None = None, service_name: str | None = None, service_tier: ServiceTier | None = None, + session_start_source: ThreadStartSource | None = None, ) -> AsyncThread: await self._ensure_initialized() params = ThreadStartParams( @@ -352,6 +361,7 @@ class AsyncCodex: sandbox=sandbox, service_name=service_name, service_tier=service_tier, + session_start_source=session_start_source, ) started = await self._client.thread_start(params) return AsyncThread(self, started.thread.id) @@ -365,6 +375,7 @@ class AsyncCodex: limit: int | None = None, model_providers: list[str] | None = None, search_term: str | None = None, + sort_direction: SortDirection | None = None, sort_key: ThreadSortKey | None = None, source_kinds: list[ThreadSourceKind] | None = None, ) -> ThreadListResponse: @@ -376,6 +387,7 @@ class AsyncCodex: limit=limit, model_providers=model_providers, search_term=search_term, + sort_direction=sort_direction, sort_key=sort_key, source_kinds=source_kinds, ) @@ -629,7 +641,9 @@ class AsyncThread: async def read(self, *, include_turns: bool = False) -> ThreadReadResponse: await self._codex._ensure_initialized() - return await self._codex._client.thread_read(self.id, include_turns=include_turns) + return await self._codex._client.thread_read( + self.id, include_turns=include_turns + ) async def set_name(self, name: str) -> ThreadSetNameResponse: await self._codex._ensure_initialized() @@ -674,7 +688,10 @@ class TurnHandle: try: for event in stream: payload = event.payload - if isinstance(payload, TurnCompletedNotification) and payload.turn.id == self.id: + if ( + isinstance(payload, TurnCompletedNotification) + and payload.turn.id == self.id + ): completed = payload finally: stream.close() @@ -725,7 +742,10 @@ class AsyncTurnHandle: try: async for event in stream: payload = event.payload - if isinstance(payload, TurnCompletedNotification) and payload.turn.id == self.id: + if ( + isinstance(payload, TurnCompletedNotification) + and payload.turn.id == self.id + ): completed = payload finally: await stream.aclose() diff --git a/sdk/python/src/codex_app_server/generated/notification_registry.py b/sdk/python/src/codex_app_server/generated/notification_registry.py index ca885d92de..ab6f87f11a 100644 --- a/sdk/python/src/codex_app_server/generated/notification_registry.py +++ b/sdk/python/src/codex_app_server/generated/notification_registry.py @@ -16,7 +16,9 @@ from .v2_all import ConfigWarningNotification from .v2_all import ContextCompactedNotification from .v2_all import DeprecationNoticeNotification from .v2_all import ErrorNotification +from .v2_all import ExternalAgentConfigImportCompletedNotification from .v2_all import FileChangeOutputDeltaNotification +from .v2_all import FileChangePatchUpdatedNotification from .v2_all import FsChangedNotification from .v2_all import FuzzyFileSearchSessionCompletedNotification from .v2_all import FuzzyFileSearchSessionUpdatedNotification @@ -44,8 +46,10 @@ from .v2_all import ThreadRealtimeClosedNotification from .v2_all import ThreadRealtimeErrorNotification from .v2_all import ThreadRealtimeItemAddedNotification from .v2_all import ThreadRealtimeOutputAudioDeltaNotification +from .v2_all import ThreadRealtimeSdpNotification from .v2_all import ThreadRealtimeStartedNotification -from .v2_all import ThreadRealtimeTranscriptUpdatedNotification +from .v2_all import ThreadRealtimeTranscriptDeltaNotification +from .v2_all import ThreadRealtimeTranscriptDoneNotification from .v2_all import ThreadStartedNotification from .v2_all import ThreadStatusChangedNotification from .v2_all import ThreadTokenUsageUpdatedNotification @@ -54,6 +58,7 @@ from .v2_all import TurnCompletedNotification from .v2_all import TurnDiffUpdatedNotification from .v2_all import TurnPlanUpdatedNotification from .v2_all import TurnStartedNotification +from .v2_all import WarningNotification from .v2_all import WindowsSandboxSetupCompletedNotification from .v2_all import WindowsWorldWritableWarningNotification @@ -66,6 +71,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "configWarning": ConfigWarningNotification, "deprecationNotice": DeprecationNoticeNotification, "error": ErrorNotification, + "externalAgentConfig/import/completed": ExternalAgentConfigImportCompletedNotification, "fs/changed": FsChangedNotification, "fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification, "fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification, @@ -78,6 +84,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "item/commandExecution/terminalInteraction": TerminalInteractionNotification, "item/completed": ItemCompletedNotification, "item/fileChange/outputDelta": FileChangeOutputDeltaNotification, + "item/fileChange/patchUpdated": FileChangePatchUpdatedNotification, "item/mcpToolCall/progress": McpToolCallProgressNotification, "item/plan/delta": PlanDeltaNotification, "item/reasoning/summaryPartAdded": ReasoningSummaryPartAddedNotification, @@ -97,8 +104,10 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "thread/realtime/error": ThreadRealtimeErrorNotification, "thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification, "thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification, + "thread/realtime/sdp": ThreadRealtimeSdpNotification, "thread/realtime/started": ThreadRealtimeStartedNotification, - "thread/realtime/transcriptUpdated": ThreadRealtimeTranscriptUpdatedNotification, + "thread/realtime/transcript/delta": ThreadRealtimeTranscriptDeltaNotification, + "thread/realtime/transcript/done": ThreadRealtimeTranscriptDoneNotification, "thread/started": ThreadStartedNotification, "thread/status/changed": ThreadStatusChangedNotification, "thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification, @@ -107,6 +116,7 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "turn/diff/updated": TurnDiffUpdatedNotification, "turn/plan/updated": TurnPlanUpdatedNotification, "turn/started": TurnStartedNotification, + "warning": WarningNotification, "windows/worldWritableWarning": WindowsWorldWritableWarningNotification, "windowsSandbox/setupCompleted": WindowsSandboxSetupCompletedNotification, } diff --git a/sdk/python/src/codex_app_server/generated/v2_all.py b/sdk/python/src/codex_app_server/generated/v2_all.py index 21d4968c33..c75b3e89af 100644 --- a/sdk/python/src/codex_app_server/generated/v2_all.py +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -42,6 +42,23 @@ class AccountLoginCompletedNotification(BaseModel): success: bool +class AddCreditsNudgeCreditType(Enum): + credits = "credits" + usage_limit = "usage_limit" + + +class AddCreditsNudgeEmailStatus(Enum): + sent = "sent" + cooldown_active = "cooldown_active" + + +class AdditionalNetworkPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool | None = None + + class AgentMessageDeltaNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -211,6 +228,18 @@ class AuthMode(Enum): chatgpt_auth_tokens = "chatgptAuthTokens" +class AutoReviewDecisionSource(RootModel[Literal["agent"]]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + Literal["agent"], + Field( + description="[UNSTABLE] Source that produced a terminal approval auto-review decision." + ), + ] + + class ByteRange(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -350,7 +379,7 @@ class ReadCommandAction(BaseModel): ) command: str name: str - path: str + path: AbsolutePathBuf type: Annotated[Literal["read"], Field(title="ReadCommandActionType")] @@ -636,14 +665,6 @@ class InputTextContentItem(BaseModel): type: Annotated[Literal["input_text"], Field(title="InputTextContentItemType")] -class InputImageContentItem(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - image_url: str - type: Annotated[Literal["input_image"], Field(title="InputImageContentItemType")] - - class OutputTextContentItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -652,15 +673,6 @@ class OutputTextContentItem(BaseModel): type: Annotated[Literal["output_text"], Field(title="OutputTextContentItemType")] -class ContentItem( - RootModel[InputTextContentItem | InputImageContentItem | OutputTextContentItem] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: InputTextContentItem | InputImageContentItem | OutputTextContentItem - - class ContextCompactedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -742,6 +754,7 @@ class DynamicToolSpec(BaseModel): description: str input_schema: Annotated[Any, Field(alias="inputSchema")] name: str + namespace: str | None = None class ExperimentalFeatureEnablementSetParams(BaseModel): @@ -810,6 +823,13 @@ class ExternalAgentConfigDetectParams(BaseModel): ] = None +class ExternalAgentConfigImportCompletedNotification(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + class ExternalAgentConfigImportResponse(BaseModel): pass model_config = ConfigDict( @@ -821,6 +841,7 @@ class ExternalAgentConfigMigrationItemType(Enum): agents_md = "AGENTS_MD" config = "CONFIG" skills = "SKILLS" + plugins = "PLUGINS" mcp_server_config = "MCP_SERVER_CONFIG" @@ -832,6 +853,7 @@ class FeedbackUploadParams(BaseModel): extra_log_files: Annotated[list[str] | None, Field(alias="extraLogFiles")] = None include_logs: Annotated[bool, Field(alias="includeLogs")] reason: str | None = None + tags: dict[str, Any] | None = None thread_id: Annotated[str | None, Field(alias="threadId")] = None @@ -852,6 +874,107 @@ class FileChangeOutputDeltaNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class FileSystemAccessMode(Enum): + read = "read" + write = "write" + none = "none" + + +class PathFileSystemPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path: AbsolutePathBuf + type: Annotated[Literal["path"], Field(title="PathFileSystemPathType")] + + +class GlobPatternFileSystemPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + pattern: str + type: Annotated[ + Literal["glob_pattern"], Field(title="GlobPatternFileSystemPathType") + ] + + +class RootFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["root"] + + +class MinimalFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["minimal"] + + +class CurrentWorkingDirectoryFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["current_working_directory"] + + +class KindFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["project_roots"] + subpath: str | None = None + + +class TmpdirFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["tmpdir"] + + +class SlashTmpFileSystemSpecialPath(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["slash_tmp"] + + +class FileSystemSpecialPath1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: Literal["unknown"] + path: str + subpath: str | None = None + + +class FileSystemSpecialPath( + RootModel[ + RootFileSystemSpecialPath + | MinimalFileSystemSpecialPath + | CurrentWorkingDirectoryFileSystemSpecialPath + | KindFileSystemSpecialPath + | TmpdirFileSystemSpecialPath + | SlashTmpFileSystemSpecialPath + | FileSystemSpecialPath1 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + RootFileSystemSpecialPath + | MinimalFileSystemSpecialPath + | CurrentWorkingDirectoryFileSystemSpecialPath + | KindFileSystemSpecialPath + | TmpdirFileSystemSpecialPath + | SlashTmpFileSystemSpecialPath + | FileSystemSpecialPath1 + ) + + class ForcedLoginMethod(Enum): chatgpt = "chatgpt" api = "api" @@ -870,7 +993,10 @@ class FsChangedNotification(BaseModel): ] watch_id: Annotated[ str, - Field(alias="watchId", description="Watch identifier returned by `fs/watch`."), + Field( + alias="watchId", + description="Watch identifier previously provided to `fs/watch`.", + ), ] @@ -941,15 +1067,19 @@ class FsGetMetadataResponse(BaseModel): is_directory: Annotated[ bool, Field( - alias="isDirectory", - description="Whether the path currently resolves to a directory.", + alias="isDirectory", description="Whether the path resolves to a directory." ), ] is_file: Annotated[ bool, Field( - alias="isFile", - description="Whether the path currently resolves to a regular file.", + alias="isFile", description="Whether the path resolves to a regular file." + ), + ] + is_symlink: Annotated[ + bool, + Field( + alias="isSymlink", description="Whether the path itself is a symbolic link." ), ] modified_at_ms: Annotated[ @@ -1054,7 +1184,10 @@ class FsUnwatchParams(BaseModel): ) watch_id: Annotated[ str, - Field(alias="watchId", description="Watch identifier returned by `fs/watch`."), + Field( + alias="watchId", + description="Watch identifier previously provided to `fs/watch`.", + ), ] @@ -1072,6 +1205,13 @@ class FsWatchParams(BaseModel): path: Annotated[ AbsolutePathBuf, Field(description="Absolute file or directory path to watch.") ] + watch_id: Annotated[ + str, + Field( + alias="watchId", + description="Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + ), + ] class FsWatchResponse(BaseModel): @@ -1082,13 +1222,6 @@ class FsWatchResponse(BaseModel): AbsolutePathBuf, Field(description="Canonicalized path associated with the watch."), ] - watch_id: Annotated[ - str, - Field( - alias="watchId", - description="Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", - ), - ] class FsWriteFileParams(BaseModel): @@ -1199,21 +1332,62 @@ class GitInfo(BaseModel): sha: str | None = None +class ApplyPatchGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: AbsolutePathBuf + files: list[AbsolutePathBuf] + type: Annotated[ + Literal["applyPatch"], Field(title="ApplyPatchGuardianApprovalReviewActionType") + ] + + +class McpToolCallGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + connector_id: Annotated[str | None, Field(alias="connectorId")] = None + connector_name: Annotated[str | None, Field(alias="connectorName")] = None + server: str + tool_name: Annotated[str, Field(alias="toolName")] + tool_title: Annotated[str | None, Field(alias="toolTitle")] = None + type: Annotated[ + Literal["mcpToolCall"], + Field(title="McpToolCallGuardianApprovalReviewActionType"), + ] + + class GuardianApprovalReviewStatus(Enum): in_progress = "inProgress" approved = "approved" denied = "denied" + timed_out = "timedOut" aborted = "aborted" +class GuardianCommandSource(Enum): + shell = "shell" + unified_exec = "unifiedExec" + + class GuardianRiskLevel(Enum): low = "low" medium = "medium" high = "high" + critical = "critical" + + +class GuardianUserAuthorization(Enum): + unknown = "unknown" + low = "low" + medium = "medium" + high = "high" class HookEventName(Enum): pre_tool_use = "preToolUse" + permission_request = "permissionRequest" post_tool_use = "postToolUse" session_start = "sessionStart" user_prompt_submit = "userPromptSubmit" @@ -1260,6 +1434,17 @@ class HookScope(Enum): turn = "turn" +class HookSource(Enum): + system = "system" + user = "user" + project = "project" + mdm = "mdm" + session_flags = "sessionFlags" + legacy_managed_config_file = "legacyManagedConfigFile" + legacy_managed_config_mdm = "legacyManagedConfigMdm" + unknown = "unknown" + + class ImageDetail(Enum): auto = "auto" low = "low" @@ -1300,22 +1485,6 @@ class InputModality(Enum): image = "image" -class ListMcpServerStatusParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - cursor: Annotated[ - str | None, - Field(description="Opaque pagination cursor returned by a previous call."), - ] = None - limit: Annotated[ - int | None, - Field( - description="Optional page size; defaults to a server-defined value.", ge=0 - ), - ] = None - - class ExecLocalShellAction(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1358,6 +1527,16 @@ class ChatgptLoginAccountParams(BaseModel): ] +class ChatgptDeviceCodeLoginAccountParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[ + Literal["chatgptDeviceCode"], + Field(title="ChatgptDeviceCodev2::LoginAccountParamsType"), + ] + + class ChatgptAuthTokensLoginAccountParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1393,6 +1572,7 @@ class LoginAccountParams( RootModel[ ApiKeyLoginAccountParams | ChatgptLoginAccountParams + | ChatgptDeviceCodeLoginAccountParams | ChatgptAuthTokensLoginAccountParams ] ): @@ -1402,6 +1582,7 @@ class LoginAccountParams( root: Annotated[ ApiKeyLoginAccountParams | ChatgptLoginAccountParams + | ChatgptDeviceCodeLoginAccountParams | ChatgptAuthTokensLoginAccountParams, Field(title="LoginAccountParams"), ] @@ -1433,6 +1614,31 @@ class ChatgptLoginAccountResponse(BaseModel): ] +class ChatgptDeviceCodeLoginAccountResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + login_id: Annotated[str, Field(alias="loginId")] + type: Annotated[ + Literal["chatgptDeviceCode"], + Field(title="ChatgptDeviceCodev2::LoginAccountResponseType"), + ] + user_code: Annotated[ + str, + Field( + alias="userCode", + description="One-time code the user must enter after signing in.", + ), + ] + verification_url: Annotated[ + str, + Field( + alias="verificationUrl", + description="URL the client should open in a browser to complete device code authorization.", + ), + ] + + class ChatgptAuthTokensLoginAccountResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1447,6 +1653,7 @@ class LoginAccountResponse( RootModel[ ApiKeyLoginAccountResponse | ChatgptLoginAccountResponse + | ChatgptDeviceCodeLoginAccountResponse | ChatgptAuthTokensLoginAccountResponse ] ): @@ -1456,6 +1663,7 @@ class LoginAccountResponse( root: Annotated[ ApiKeyLoginAccountResponse | ChatgptLoginAccountResponse + | ChatgptDeviceCodeLoginAccountResponse | ChatgptAuthTokensLoginAccountResponse, Field(title="LoginAccountResponse"), ] @@ -1468,6 +1676,24 @@ class LogoutAccountResponse(BaseModel): ) +class MarketplaceAddParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + ref_name: Annotated[str | None, Field(alias="refName")] = None + source: str + sparse_paths: Annotated[list[str] | None, Field(alias="sparsePaths")] = None + + +class MarketplaceAddResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + already_added: Annotated[bool, Field(alias="alreadyAdded")] + installed_root: Annotated[AbsolutePathBuf, Field(alias="installedRoot")] + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + + class MarketplaceInterface(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1483,6 +1709,23 @@ class MarketplaceLoadErrorInfo(BaseModel): message: str +class MarketplaceRemoveParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + + +class MarketplaceRemoveResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + installed_root: Annotated[AbsolutePathBuf | None, Field(alias="installedRoot")] = ( + None + ) + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + + class McpAuthStatus(Enum): unsupported = "unsupported" not_logged_in = "notLoggedIn" @@ -1490,6 +1733,15 @@ class McpAuthStatus(Enum): o_auth = "oAuth" +class McpResourceReadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + server: str + thread_id: Annotated[str | None, Field(alias="threadId")] = None + uri: str + + class McpServerOauthLoginCompletedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1529,6 +1781,11 @@ class McpServerStartupState(Enum): cancelled = "cancelled" +class McpServerStatusDetail(Enum): + full = "full" + tools_and_auth_only = "toolsAndAuthOnly" + + class McpServerStatusUpdatedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1538,6 +1795,27 @@ class McpServerStatusUpdatedNotification(BaseModel): status: McpServerStartupState +class McpServerToolCallParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Annotated[Any | None, Field(alias="_meta")] = None + arguments: Any | None = None + server: str + thread_id: Annotated[str, Field(alias="threadId")] + tool: str + + +class McpServerToolCallResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Annotated[Any | None, Field(alias="_meta")] = None + content: list + is_error: Annotated[bool | None, Field(alias="isError")] = None + structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None + + class McpToolCallError(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1559,6 +1837,7 @@ class McpToolCallResult(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + field_meta: Annotated[Any | None, Field(alias="_meta")] = None content: list structured_content: Annotated[Any | None, Field(alias="structuredContent")] = None @@ -1658,28 +1937,81 @@ class NetworkAccess(Enum): enabled = "enabled" +class NetworkApprovalProtocol(Enum): + http = "http" + https = "https" + socks5_tcp = "socks5Tcp" + socks5_udp = "socks5Udp" + + +class NetworkDomainPermission(Enum): + allow = "allow" + deny = "deny" + + class NetworkRequirements(BaseModel): model_config = ConfigDict( populate_by_name=True, ) allow_local_binding: Annotated[bool | None, Field(alias="allowLocalBinding")] = None - allow_unix_sockets: Annotated[list[str] | None, Field(alias="allowUnixSockets")] = ( - None - ) + allow_unix_sockets: Annotated[ + list[str] | None, + Field( + alias="allowUnixSockets", + description="Legacy compatibility view derived from `unix_sockets`.", + ), + ] = None allow_upstream_proxy: Annotated[bool | None, Field(alias="allowUpstreamProxy")] = ( None ) - allowed_domains: Annotated[list[str] | None, Field(alias="allowedDomains")] = None + allowed_domains: Annotated[ + list[str] | None, + Field( + alias="allowedDomains", + description="Legacy compatibility view derived from `domains`.", + ), + ] = None dangerously_allow_all_unix_sockets: Annotated[ bool | None, Field(alias="dangerouslyAllowAllUnixSockets") ] = None dangerously_allow_non_loopback_proxy: Annotated[ bool | None, Field(alias="dangerouslyAllowNonLoopbackProxy") ] = None - denied_domains: Annotated[list[str] | None, Field(alias="deniedDomains")] = None + denied_domains: Annotated[ + list[str] | None, + Field( + alias="deniedDomains", + description="Legacy compatibility view derived from `domains`.", + ), + ] = None + domains: Annotated[ + dict[str, Any] | None, + Field( + description="Canonical network permission map for `experimental_network`." + ), + ] = None enabled: bool | None = None http_port: Annotated[int | None, Field(alias="httpPort", ge=0)] = None + managed_allowed_domains_only: Annotated[ + bool | None, + Field( + alias="managedAllowedDomainsOnly", + description="When true, only managed allowlist entries are respected while managed network enforcement is active.", + ), + ] = None socks_port: Annotated[int | None, Field(alias="socksPort", ge=0)] = None + unix_sockets: Annotated[ + dict[str, Any] | None, + Field( + alias="unixSockets", + description="Canonical unix socket permission map for `experimental_network`.", + ), + ] = None + + +class NetworkUnixSocketPermission(Enum): + allow = "allow" + none = "none" class NonSteerableTurnKind(Enum): @@ -1746,6 +2078,7 @@ class PlanType(Enum): go = "go" plus = "plus" pro = "pro" + prolite = "prolite" team = "team" self_serve_business_usage_based = "self_serve_business_usage_based" business = "business" @@ -1764,15 +2097,13 @@ class PluginInstallParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - force_remote_sync: Annotated[ - bool | None, - Field( - alias="forceRemoteSync", - description="When true, apply the remote plugin change before the local install flow.", - ), + marketplace_path: Annotated[ + AbsolutePathBuf | None, Field(alias="marketplacePath") ] = None - marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")] plugin_name: Annotated[str, Field(alias="pluginName")] + remote_marketplace_name: Annotated[ + str | None, Field(alias="remoteMarketplaceName") + ] = None class PluginInstallPolicy(Enum): @@ -1796,7 +2127,20 @@ class PluginInterface(BaseModel): brand_color: Annotated[str | None, Field(alias="brandColor")] = None capabilities: list[str] category: str | None = None - composer_icon: Annotated[AbsolutePathBuf | None, Field(alias="composerIcon")] = None + composer_icon: Annotated[ + AbsolutePathBuf | None, + Field( + alias="composerIcon", + description="Local composer icon path, resolved from the installed plugin package.", + ), + ] = None + composer_icon_url: Annotated[ + str | None, + Field( + alias="composerIconUrl", + description="Remote composer icon URL from the plugin catalog.", + ), + ] = None default_prompt: Annotated[ list[str] | None, Field( @@ -1806,10 +2150,31 @@ class PluginInterface(BaseModel): ] = None developer_name: Annotated[str | None, Field(alias="developerName")] = None display_name: Annotated[str | None, Field(alias="displayName")] = None - logo: AbsolutePathBuf | None = None + logo: Annotated[ + AbsolutePathBuf | None, + Field( + description="Local logo path, resolved from the installed plugin package." + ), + ] = None + logo_url: Annotated[ + str | None, + Field(alias="logoUrl", description="Remote logo URL from the plugin catalog."), + ] = None long_description: Annotated[str | None, Field(alias="longDescription")] = None privacy_policy_url: Annotated[str | None, Field(alias="privacyPolicyUrl")] = None - screenshots: list[AbsolutePathBuf] + screenshot_urls: Annotated[ + list[str], + Field( + alias="screenshotUrls", + description="Remote screenshot URLs from the plugin catalog.", + ), + ] + screenshots: Annotated[ + list[AbsolutePathBuf], + Field( + description="Local screenshot paths, resolved from the installed plugin package." + ), + ] short_description: Annotated[str | None, Field(alias="shortDescription")] = None terms_of_service_url: Annotated[str | None, Field(alias="termsOfServiceUrl")] = None website_url: Annotated[str | None, Field(alias="websiteUrl")] = None @@ -1825,21 +2190,19 @@ class PluginListParams(BaseModel): description="Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered." ), ] = None - force_remote_sync: Annotated[ - bool | None, - Field( - alias="forceRemoteSync", - description="When true, reconcile the official curated marketplace against the remote plugin state before listing marketplaces.", - ), - ] = None class PluginReadParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")] + marketplace_path: Annotated[ + AbsolutePathBuf | None, Field(alias="marketplacePath") + ] = None plugin_name: Annotated[str, Field(alias="pluginName")] + remote_marketplace_name: Annotated[ + str | None, Field(alias="remoteMarketplaceName") + ] = None class LocalPluginSource(BaseModel): @@ -1850,11 +2213,29 @@ class LocalPluginSource(BaseModel): type: Annotated[Literal["local"], Field(title="LocalPluginSourceType")] -class PluginSource(RootModel[LocalPluginSource]): +class GitPluginSource(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - root: LocalPluginSource + path: str | None = None + ref_name: Annotated[str | None, Field(alias="refName")] = None + sha: str | None = None + type: Annotated[Literal["git"], Field(title="GitPluginSourceType")] + url: str + + +class RemotePluginSource(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[Literal["remote"], Field(title="RemotePluginSourceType")] + + +class PluginSource(RootModel[LocalPluginSource | GitPluginSource | RemotePluginSource]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: LocalPluginSource | GitPluginSource | RemotePluginSource class PluginSummary(BaseModel): @@ -1875,13 +2256,6 @@ class PluginUninstallParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - force_remote_sync: Annotated[ - bool | None, - Field( - alias="forceRemoteSync", - description="When true, apply the remote plugin change before the local uninstall flow.", - ), - ] = None plugin_id: Annotated[str, Field(alias="pluginId")] @@ -1892,6 +2266,22 @@ class PluginUninstallResponse(BaseModel): ) +class PluginsMigration(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + plugin_names: Annotated[list[str], Field(alias="pluginNames")] + + +class RateLimitReachedType(Enum): + rate_limit_reached = "rate_limit_reached" + workspace_owner_credits_depleted = "workspace_owner_credits_depleted" + workspace_member_credits_depleted = "workspace_member_credits_depleted" + workspace_owner_usage_limit_reached = "workspace_owner_usage_limit_reached" + workspace_member_usage_limit_reached = "workspace_member_usage_limit_reached" + + class RateLimitWindow(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1935,6 +2325,43 @@ class RealtimeConversationVersion(Enum): v2 = "v2" +class RealtimeOutputModality(Enum): + text = "text" + audio = "audio" + + +class RealtimeVoice(Enum): + alloy = "alloy" + arbor = "arbor" + ash = "ash" + ballad = "ballad" + breeze = "breeze" + cedar = "cedar" + coral = "coral" + cove = "cove" + echo = "echo" + ember = "ember" + juniper = "juniper" + maple = "maple" + marin = "marin" + sage = "sage" + shimmer = "shimmer" + sol = "sol" + spruce = "spruce" + vale = "vale" + verse = "verse" + + +class RealtimeVoicesList(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + default_v1: Annotated[RealtimeVoice, Field(alias="defaultV1")] + default_v2: Annotated[RealtimeVoice, Field(alias="defaultV2")] + v1: list[RealtimeVoice] + v2: list[RealtimeVoice] + + class ReasoningEffort(Enum): none = "none" minimal = "minimal" @@ -2078,6 +2505,38 @@ class Resource(BaseModel): uri: str +class ResourceContent1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Annotated[Any | None, Field(alias="_meta")] = None + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + text: str + uri: Annotated[str, Field(description="The URI of this resource.")] + + +class ResourceContent2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Annotated[Any | None, Field(alias="_meta")] = None + blob: str + mime_type: Annotated[str | None, Field(alias="mimeType")] = None + uri: Annotated[str, Field(description="The URI of this resource.")] + + +class ResourceContent(RootModel[ResourceContent1 | ResourceContent2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + ResourceContent1 | ResourceContent2, + Field( + description="Contents returned when reading a resource from an MCP server." + ), + ] + + class ResourceTemplate(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2090,18 +2549,6 @@ class ResourceTemplate(BaseModel): uri_template: Annotated[str, Field(alias="uriTemplate")] -class MessageResponseItem(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - content: list[ContentItem] - end_turn: bool | None = None - id: str | None = None - phase: MessagePhase | None = None - role: str - type: Annotated[Literal["message"], Field(title="MessageResponseItemType")] - - class ReasoningResponseItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2435,6 +2882,20 @@ class SandboxWorkspaceWrite(BaseModel): writable_roots: list[str] | None = [] +class SendAddCreditsNudgeEmailParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + credit_type: Annotated[AddCreditsNudgeCreditType, Field(alias="creditType")] + + +class SendAddCreditsNudgeEmailResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: AddCreditsNudgeEmailStatus + + class ItemAgentMessageDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2511,6 +2972,17 @@ class McpServerStartupStatusUpdatedServerNotification(BaseModel): params: McpServerStatusUpdatedNotification +class ExternalAgentConfigImportCompletedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["externalAgentConfig/import/completed"], + Field(title="ExternalAgentConfig/import/completedNotificationMethod"), + ] + params: ExternalAgentConfigImportCompletedNotification + + class FsChangedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2670,8 +3142,8 @@ class SkillInterface(BaseModel): brand_color: Annotated[str | None, Field(alias="brandColor")] = None default_prompt: Annotated[str | None, Field(alias="defaultPrompt")] = None display_name: Annotated[str | None, Field(alias="displayName")] = None - icon_large: Annotated[str | None, Field(alias="iconLarge")] = None - icon_small: Annotated[str | None, Field(alias="iconSmall")] = None + icon_large: Annotated[AbsolutePathBuf | None, Field(alias="iconLarge")] = None + icon_small: Annotated[AbsolutePathBuf | None, Field(alias="iconSmall")] = None short_description: Annotated[str | None, Field(alias="shortDescription")] = None @@ -2690,7 +3162,7 @@ class SkillSummary(BaseModel): enabled: bool interface: SkillInterface | None = None name: str - path: str + path: AbsolutePathBuf short_description: Annotated[str | None, Field(alias="shortDescription")] = None @@ -2765,6 +3237,11 @@ class SkillsListParams(BaseModel): ] = None +class SortDirection(Enum): + asc = "asc" + desc = "desc" + + class SubAgentSourceValue(Enum): review = "review" compact = "compact" @@ -2913,6 +3390,26 @@ class ThreadId(RootModel[str]): root: str +class ThreadInjectItemsParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + items: Annotated[ + list, + Field( + description="Raw Responses API items to append to the thread's model-visible history." + ), + ] + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadInjectItemsResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + class HookPromptThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2960,7 +3457,9 @@ class CommandExecutionThreadItem(BaseModel): description="A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", ), ] - cwd: Annotated[str, Field(description="The command's working directory.")] + cwd: Annotated[ + AbsolutePathBuf, Field(description="The command's working directory.") + ] duration_ms: Annotated[ int | None, Field( @@ -3000,6 +3499,7 @@ class McpToolCallThreadItem(BaseModel): ] = None error: McpToolCallError | None = None id: str + mcp_app_resource_uri: Annotated[str | None, Field(alias="mcpAppResourceUri")] = None result: McpToolCallResult | None = None server: str status: McpToolCallStatus @@ -3023,6 +3523,7 @@ class DynamicToolCallThreadItem(BaseModel): ), ] = None id: str + namespace: str | None = None status: DynamicToolCallStatus success: bool | None = None tool: str @@ -3036,7 +3537,7 @@ class ImageViewThreadItem(BaseModel): populate_by_name=True, ) id: str - path: str + path: AbsolutePathBuf type: Annotated[Literal["imageView"], Field(title="ImageViewThreadItemType")] @@ -3047,7 +3548,7 @@ class ImageGenerationThreadItem(BaseModel): id: str result: str revised_prompt: Annotated[str | None, Field(alias="revisedPrompt")] = None - saved_path: Annotated[str | None, Field(alias="savedPath")] = None + saved_path: Annotated[AbsolutePathBuf | None, Field(alias="savedPath")] = None status: str type: Annotated[ Literal["imageGeneration"], Field(title="ImageGenerationThreadItemType") @@ -3116,6 +3617,11 @@ class ThreadLoadedListResponse(BaseModel): ] = None +class ThreadMemoryMode(Enum): + enabled = "enabled" + disabled = "disabled" + + class ThreadMetadataGitInfoUpdateParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3222,6 +3728,52 @@ class ThreadRealtimeOutputAudioDeltaNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] +class ThreadRealtimeSdpNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + sdp: str + thread_id: Annotated[str, Field(alias="threadId")] + + +class WebsocketThreadRealtimeStartTransport(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[ + Literal["websocket"], Field(title="WebsocketThreadRealtimeStartTransportType") + ] + + +class WebrtcThreadRealtimeStartTransport(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + sdp: Annotated[ + str, + Field( + description="SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel." + ), + ] + type: Annotated[ + Literal["webrtc"], Field(title="WebrtcThreadRealtimeStartTransportType") + ] + + +class ThreadRealtimeStartTransport( + RootModel[ + WebsocketThreadRealtimeStartTransport | WebrtcThreadRealtimeStartTransport + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + WebsocketThreadRealtimeStartTransport | WebrtcThreadRealtimeStartTransport, + Field(description="EXPERIMENTAL - transport used by thread realtime."), + ] + + class ThreadRealtimeStartedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3231,12 +3783,25 @@ class ThreadRealtimeStartedNotification(BaseModel): version: RealtimeConversationVersion -class ThreadRealtimeTranscriptUpdatedNotification(BaseModel): +class ThreadRealtimeTranscriptDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: Annotated[ + str, Field(description="Live transcript delta from the realtime event.") + ] + role: str + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadRealtimeTranscriptDoneNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) role: str - text: str + text: Annotated[ + str, Field(description="Final complete text for the transcript part.") + ] thread_id: Annotated[str, Field(alias="threadId")] @@ -3339,33 +3904,9 @@ class ThreadSourceKind(Enum): unknown = "unknown" -class ThreadStartParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( - None - ) - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, - Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this thread and subsequent turns.", - ), - ] = None - base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None - config: dict[str, Any] | None = None - cwd: str | None = None - developer_instructions: Annotated[ - str | None, Field(alias="developerInstructions") - ] = None - ephemeral: bool | None = None - model: str | None = None - model_provider: Annotated[str | None, Field(alias="modelProvider")] = None - personality: Personality | None = None - sandbox: SandboxMode | None = None - service_name: Annotated[str | None, Field(alias="serviceName")] = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None +class ThreadStartSource(Enum): + startup = "startup" + clear = "clear" class NotLoadedThreadStatus(BaseModel): @@ -3424,6 +3965,29 @@ class ThreadStatusChangedNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] +class ThreadTurnsListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: Annotated[ + str | None, + Field( + description="Opaque cursor to pass to the next call to continue after the last turn." + ), + ] = None + limit: Annotated[ + int | None, Field(description="Optional turn page size.", ge=0) + ] = None + sort_direction: Annotated[ + SortDirection | None, + Field( + alias="sortDirection", + description="Optional turn pagination direction; defaults to descending.", + ), + ] = None + thread_id: Annotated[str, Field(alias="threadId")] + + class ThreadUnarchiveParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3595,6 +4159,20 @@ class Verbosity(Enum): high = "high" +class WarningNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: Annotated[str, Field(description="Concise warning message for the user.")] + thread_id: Annotated[ + str | None, + Field( + alias="threadId", + description="Optional thread target when the warning applies to a specific thread.", + ), + ] = None + + class SearchWebSearchAction(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3795,15 +4373,6 @@ class InitializeRequest(BaseModel): params: InitializeParams -class ThreadStartRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["thread/start"], Field(title="Thread/startRequestMethod")] - params: ThreadStartParams - - class ThreadResumeRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3934,6 +4503,28 @@ class ThreadReadRequest(BaseModel): params: ThreadReadParams +class ThreadTurnsListRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/turns/list"], Field(title="Thread/turns/listRequestMethod") + ] + params: ThreadTurnsListParams + + +class ThreadInjectItemsRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/inject_items"], Field(title="Thread/injectItemsRequestMethod") + ] + params: ThreadInjectItemsParams + + class SkillsListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3943,6 +4534,28 @@ class SkillsListRequest(BaseModel): params: SkillsListParams +class MarketplaceAddRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["marketplace/add"], Field(title="Marketplace/addRequestMethod") + ] + params: MarketplaceAddParams + + +class MarketplaceRemoveRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["marketplace/remove"], Field(title="Marketplace/removeRequestMethod") + ] + params: MarketplaceRemoveParams + + class PluginListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4158,16 +4771,27 @@ class ConfigMcpServerReloadRequest(BaseModel): params: None = None -class McpServerStatusListRequest(BaseModel): +class McpServerResourceReadRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, ) id: RequestId method: Annotated[ - Literal["mcpServerStatus/list"], - Field(title="McpServerStatus/listRequestMethod"), + Literal["mcpServer/resource/read"], + Field(title="McpServer/resource/readRequestMethod"), ] - params: ListMcpServerStatusParams + params: McpResourceReadParams + + +class McpServerToolCallRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["mcpServer/tool/call"], Field(title="McpServer/tool/callRequestMethod") + ] + params: McpServerToolCallParams class WindowsSandboxSetupStartRequest(BaseModel): @@ -4228,6 +4852,18 @@ class AccountRateLimitsReadRequest(BaseModel): params: None = None +class AccountSendAddCreditsNudgeEmailRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["account/sendAddCreditsNudgeEmail"], + Field(title="Account/sendAddCreditsNudgeEmailRequestMethod"), + ] + params: SendAddCreditsNudgeEmailParams + + class FeedbackUploadRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4613,6 +5249,24 @@ class ConfigWarningNotification(BaseModel): summary: Annotated[str, Field(description="Concise summary of the warning.")] +class InputImageContentItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + detail: ImageDetail | None = None + image_url: str + type: Annotated[Literal["input_image"], Field(title="InputImageContentItemType")] + + +class ContentItem( + RootModel[InputTextContentItem | InputImageContentItem | OutputTextContentItem] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: InputTextContentItem | InputImageContentItem | OutputTextContentItem + + class ExperimentalFeature(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4672,18 +5326,29 @@ class ExperimentalFeatureListResponse(BaseModel): ] = None -class ExternalAgentConfigMigrationItem(BaseModel): +class SpecialFileSystemPath(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - cwd: Annotated[ - str | None, - Field( - description="Null or empty means home-scoped migration; non-empty means repo-scoped migration." - ), - ] = None - description: str - item_type: Annotated[ExternalAgentConfigMigrationItemType, Field(alias="itemType")] + type: Annotated[Literal["special"], Field(title="SpecialFileSystemPathType")] + value: FileSystemSpecialPath + + +class FileSystemPath( + RootModel[PathFileSystemPath | GlobPatternFileSystemPath | SpecialFileSystemPath] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: PathFileSystemPath | GlobPatternFileSystemPath | SpecialFileSystemPath + + +class FileSystemSandboxEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + access: FileSystemAccessMode + path: FileSystemPath class FileUpdateChange(BaseModel): @@ -4738,8 +5403,49 @@ class GuardianApprovalReview(BaseModel): ) rationale: str | None = None risk_level: Annotated[GuardianRiskLevel | None, Field(alias="riskLevel")] = None - risk_score: Annotated[int | None, Field(alias="riskScore", ge=0)] = None status: GuardianApprovalReviewStatus + user_authorization: Annotated[ + GuardianUserAuthorization | None, Field(alias="userAuthorization") + ] = None + + +class CommandGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str + cwd: AbsolutePathBuf + source: GuardianCommandSource + type: Annotated[ + Literal["command"], Field(title="CommandGuardianApprovalReviewActionType") + ] + + +class ExecveGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + argv: list[str] + cwd: AbsolutePathBuf + program: str + source: GuardianCommandSource + type: Annotated[ + Literal["execve"], Field(title="ExecveGuardianApprovalReviewActionType") + ] + + +class NetworkAccessGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + host: str + port: Annotated[int, Field(ge=0)] + protocol: NetworkApprovalProtocol + target: str + type: Annotated[ + Literal["networkAccess"], + Field(title="NetworkAccessGuardianApprovalReviewActionType"), + ] class HookOutputEntry(BaseModel): @@ -4763,7 +5469,8 @@ class HookRunSummary(BaseModel): handler_type: Annotated[HookHandlerType, Field(alias="handlerType")] id: str scope: HookScope - source_path: Annotated[str, Field(alias="sourcePath")] + source: HookSource | None = "unknown" + source_path: Annotated[AbsolutePathBuf, Field(alias="sourcePath")] started_at: Annotated[int, Field(alias="startedAt")] status: HookRunStatus status_message: Annotated[str | None, Field(alias="statusMessage")] = None @@ -4778,26 +5485,33 @@ class HookStartedNotification(BaseModel): turn_id: Annotated[str | None, Field(alias="turnId")] = None -class ItemGuardianApprovalReviewCompletedNotification(BaseModel): +class ListMcpServerStatusParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - action: Any | None = None - review: GuardianApprovalReview - target_item_id: Annotated[str, Field(alias="targetItemId")] - thread_id: Annotated[str, Field(alias="threadId")] - turn_id: Annotated[str, Field(alias="turnId")] + cursor: Annotated[ + str | None, + Field(description="Opaque pagination cursor returned by a previous call."), + ] = None + detail: Annotated[ + McpServerStatusDetail | None, + Field( + description="Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + ), + ] = None + limit: Annotated[ + int | None, + Field( + description="Optional page size; defaults to a server-defined value.", ge=0 + ), + ] = None -class ItemGuardianApprovalReviewStartedNotification(BaseModel): +class McpResourceReadResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - action: Any | None = None - review: GuardianApprovalReview - target_item_id: Annotated[str, Field(alias="targetItemId")] - thread_id: Annotated[str, Field(alias="threadId")] - turn_id: Annotated[str, Field(alias="turnId")] + contents: list[ResourceContent] class McpServerStatus(BaseModel): @@ -4821,10 +5535,20 @@ class MemoryCitation(BaseModel): thread_ids: Annotated[list[str], Field(alias="threadIds")] +class MigrationDetails(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + plugins: list[PluginsMigration] + + class Model(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + additional_speed_tiers: Annotated[ + list[str] | None, Field(alias="additionalSpeedTiers") + ] = [] availability_nux: Annotated[ ModelAvailabilityNux | None, Field(alias="availabilityNux") ] = None @@ -4892,7 +5616,12 @@ class PluginMarketplaceEntry(BaseModel): ) interface: MarketplaceInterface | None = None name: str - path: AbsolutePathBuf + path: Annotated[ + AbsolutePathBuf | None, + Field( + description="Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." + ), + ] = None plugins: list[PluginSummary] @@ -4912,9 +5641,24 @@ class RateLimitSnapshot(BaseModel): limit_name: Annotated[str | None, Field(alias="limitName")] = None plan_type: Annotated[PlanType | None, Field(alias="planType")] = None primary: RateLimitWindow | None = None + rate_limit_reached_type: Annotated[ + RateLimitReachedType | None, Field(alias="rateLimitReachedType") + ] = None secondary: RateLimitWindow | None = None +class MessageResponseItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: list[ContentItem] + end_turn: bool | None = None + id: str | None = None + phase: MessagePhase | None = None + role: str + type: Annotated[Literal["message"], Field(title="MessageResponseItemType")] + + class WebSearchCallResponseItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5023,28 +5767,6 @@ class TurnDiffUpdatedServerNotification(BaseModel): params: TurnDiffUpdatedNotification -class ItemAutoApprovalReviewStartedServerNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - method: Annotated[ - Literal["item/autoApprovalReview/started"], - Field(title="Item/autoApprovalReview/startedNotificationMethod"), - ] - params: ItemGuardianApprovalReviewStartedNotification - - -class ItemAutoApprovalReviewCompletedServerNotification(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - method: Annotated[ - Literal["item/autoApprovalReview/completed"], - Field(title="Item/autoApprovalReview/completedNotificationMethod"), - ] - params: ItemGuardianApprovalReviewCompletedNotification - - class CommandExecOutputDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5088,6 +5810,14 @@ class AccountUpdatedServerNotification(BaseModel): params: AccountUpdatedNotification +class WarningServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[Literal["warning"], Field(title="WarningNotificationMethod")] + params: WarningNotification + + class ConfigWarningServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5120,15 +5850,26 @@ class ThreadRealtimeItemAddedServerNotification(BaseModel): params: ThreadRealtimeItemAddedNotification -class ThreadRealtimeTranscriptUpdatedServerNotification(BaseModel): +class ThreadRealtimeTranscriptDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) method: Annotated[ - Literal["thread/realtime/transcriptUpdated"], - Field(title="Thread/realtime/transcriptUpdatedNotificationMethod"), + Literal["thread/realtime/transcript/delta"], + Field(title="Thread/realtime/transcript/deltaNotificationMethod"), ] - params: ThreadRealtimeTranscriptUpdatedNotification + params: ThreadRealtimeTranscriptDeltaNotification + + +class ThreadRealtimeTranscriptDoneServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["thread/realtime/transcript/done"], + Field(title="Thread/realtime/transcript/doneNotificationMethod"), + ] + params: ThreadRealtimeTranscriptDoneNotification class ThreadRealtimeOutputAudioDeltaServerNotification(BaseModel): @@ -5142,6 +5883,17 @@ class ThreadRealtimeOutputAudioDeltaServerNotification(BaseModel): params: ThreadRealtimeOutputAudioDeltaNotification +class ThreadRealtimeSdpServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["thread/realtime/sdp"], + Field(title="Thread/realtime/sdpNotificationMethod"), + ] + params: ThreadRealtimeSdpNotification + + class ThreadRealtimeErrorServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5191,7 +5943,7 @@ class SkillMetadata(BaseModel): enabled: bool interface: SkillInterface | None = None name: str - path: str + path: AbsolutePathBuf scope: SkillScope short_description: Annotated[ str | None, @@ -5429,6 +6181,13 @@ class ThreadListParams(BaseModel): description="Optional substring filter for the extracted thread title.", ), ] = None + sort_direction: Annotated[ + SortDirection | None, + Field( + alias="sortDirection", + description="Optional sort direction; defaults to descending (newest first).", + ), + ] = None sort_key: Annotated[ ThreadSortKey | None, Field( @@ -5444,6 +6203,38 @@ class ThreadListParams(BaseModel): ] = None +class ThreadStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( + None + ) + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this thread and subsequent turns.", + ), + ] = None + base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None + config: dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: Annotated[ + str | None, Field(alias="developerInstructions") + ] = None + ephemeral: bool | None = None + model: str | None = None + model_provider: Annotated[str | None, Field(alias="modelProvider")] = None + personality: Personality | None = None + sandbox: SandboxMode | None = None + service_name: Annotated[str | None, Field(alias="serviceName")] = None + service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + session_start_source: Annotated[ + ThreadStartSource | None, Field(alias="sessionStartSource") + ] = None + + class ThreadTokenUsage(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5610,6 +6401,18 @@ class AccountRateLimitsUpdatedNotification(BaseModel): rate_limits: Annotated[RateLimitSnapshot, Field(alias="rateLimits")] +class AdditionalFileSystemPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + entries: list[FileSystemSandboxEntry] | None = None + glob_scan_max_depth: Annotated[ + int | None, Field(alias="globScanMaxDepth", ge=1) + ] = None + read: list[AbsolutePathBuf] | None = None + write: list[AbsolutePathBuf] | None = None + + class AppInfo(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5660,6 +6463,15 @@ class AppsListResponse(BaseModel): ] = None +class ThreadStartRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["thread/start"], Field(title="Thread/startRequestMethod")] + params: ThreadStartParams + + class ThreadListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5696,6 +6508,18 @@ class ReviewStartRequest(BaseModel): params: ReviewStartParams +class McpServerStatusListRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["mcpServerStatus/list"], + Field(title="McpServerStatus/listRequestMethod"), + ] + params: ListMcpServerStatusParams + + class CommandExecRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5777,20 +6601,29 @@ class ErrorNotification(BaseModel): will_retry: Annotated[bool, Field(alias="willRetry")] -class ExternalAgentConfigDetectResponse(BaseModel): +class ExternalAgentConfigMigrationItem(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - items: list[ExternalAgentConfigMigrationItem] + cwd: Annotated[ + str | None, + Field( + description="Null or empty means home-scoped migration; non-empty means repo-scoped migration." + ), + ] = None + description: str + details: MigrationDetails | None = None + item_type: Annotated[ExternalAgentConfigMigrationItemType, Field(alias="itemType")] -class ExternalAgentConfigImportParams(BaseModel): +class FileChangePatchUpdatedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - migration_items: Annotated[ - list[ExternalAgentConfigMigrationItem], Field(alias="migrationItems") - ] + changes: list[FileUpdateChange] + item_id: Annotated[str, Field(alias="itemId")] + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] class FunctionCallOutputBody(RootModel[str | list[FunctionCallOutputContentItem]]): @@ -5872,7 +6705,6 @@ class PluginListResponse(BaseModel): list[MarketplaceLoadErrorInfo] | None, Field(alias="marketplaceLoadErrors") ] = [] marketplaces: list[PluginMarketplaceEntry] - remote_sync_error: Annotated[str | None, Field(alias="remoteSyncError")] = None class ProfileV2(BaseModel): @@ -5898,6 +6730,17 @@ class ProfileV2(BaseModel): web_search: WebSearchMode | None = None +class RequestPermissionProfile(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + file_system: Annotated[ + AdditionalFileSystemPermissions | None, Field(alias="fileSystem") + ] = None + network: AdditionalNetworkPermissions | None = None + + class FunctionCallOutputResponseItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6021,6 +6864,17 @@ class ItemCompletedServerNotification(BaseModel): params: ItemCompletedNotification +class ItemFileChangePatchUpdatedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["item/fileChange/patchUpdated"], + Field(title="Item/fileChange/patchUpdatedNotificationMethod"), + ] + params: FileChangePatchUpdatedNotification + + class AccountRateLimitsUpdatedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6074,6 +6928,20 @@ class Turn(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at: Annotated[ + int | None, + Field( + alias="completedAt", + description="Unix timestamp (in seconds) when the turn completed.", + ), + ] = None + duration_ms: Annotated[ + int | None, + Field( + alias="durationMs", + description="Duration between turn start and completion in milliseconds, if known.", + ), + ] = None error: Annotated[ TurnError | None, Field(description="Only populated when the Turn's status is failed."), @@ -6085,6 +6953,13 @@ class Turn(BaseModel): description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." ), ] + started_at: Annotated[ + int | None, + Field( + alias="startedAt", + description="Unix timestamp (in seconds) when the turn started.", + ), + ] = None status: TurnStatus @@ -6111,18 +6986,6 @@ class TurnStartedNotification(BaseModel): turn: Turn -class ExternalAgentConfigImportRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["externalAgentConfig/import"], - Field(title="ExternalAgentConfig/importRequestMethod"), - ] - params: ExternalAgentConfigImportParams - - class ConfigBatchWriteRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6134,138 +6997,6 @@ class ConfigBatchWriteRequest(BaseModel): params: ConfigBatchWriteParams -class ClientRequest( - RootModel[ - InitializeRequest - | ThreadStartRequest - | ThreadResumeRequest - | ThreadForkRequest - | ThreadArchiveRequest - | ThreadUnsubscribeRequest - | ThreadNameSetRequest - | ThreadMetadataUpdateRequest - | ThreadUnarchiveRequest - | ThreadCompactStartRequest - | ThreadShellCommandRequest - | ThreadRollbackRequest - | ThreadListRequest - | ThreadLoadedListRequest - | ThreadReadRequest - | SkillsListRequest - | PluginListRequest - | PluginReadRequest - | AppListRequest - | FsReadFileRequest - | FsWriteFileRequest - | FsCreateDirectoryRequest - | FsGetMetadataRequest - | FsReadDirectoryRequest - | FsRemoveRequest - | FsCopyRequest - | FsWatchRequest - | FsUnwatchRequest - | SkillsConfigWriteRequest - | PluginInstallRequest - | PluginUninstallRequest - | TurnStartRequest - | TurnSteerRequest - | TurnInterruptRequest - | ReviewStartRequest - | ModelListRequest - | ExperimentalFeatureListRequest - | ExperimentalFeatureEnablementSetRequest - | McpServerOauthLoginRequest - | ConfigMcpServerReloadRequest - | McpServerStatusListRequest - | WindowsSandboxSetupStartRequest - | AccountLoginStartRequest - | AccountLoginCancelRequest - | AccountLogoutRequest - | AccountRateLimitsReadRequest - | FeedbackUploadRequest - | CommandExecRequest - | CommandExecWriteRequest - | CommandExecTerminateRequest - | CommandExecResizeRequest - | ConfigReadRequest - | ExternalAgentConfigDetectRequest - | ExternalAgentConfigImportRequest - | ConfigValueWriteRequest - | ConfigBatchWriteRequest - | ConfigRequirementsReadRequest - | AccountReadRequest - | FuzzyFileSearchRequest - ] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - InitializeRequest - | ThreadStartRequest - | ThreadResumeRequest - | ThreadForkRequest - | ThreadArchiveRequest - | ThreadUnsubscribeRequest - | ThreadNameSetRequest - | ThreadMetadataUpdateRequest - | ThreadUnarchiveRequest - | ThreadCompactStartRequest - | ThreadShellCommandRequest - | ThreadRollbackRequest - | ThreadListRequest - | ThreadLoadedListRequest - | ThreadReadRequest - | SkillsListRequest - | PluginListRequest - | PluginReadRequest - | AppListRequest - | FsReadFileRequest - | FsWriteFileRequest - | FsCreateDirectoryRequest - | FsGetMetadataRequest - | FsReadDirectoryRequest - | FsRemoveRequest - | FsCopyRequest - | FsWatchRequest - | FsUnwatchRequest - | SkillsConfigWriteRequest - | PluginInstallRequest - | PluginUninstallRequest - | TurnStartRequest - | TurnSteerRequest - | TurnInterruptRequest - | ReviewStartRequest - | ModelListRequest - | ExperimentalFeatureListRequest - | ExperimentalFeatureEnablementSetRequest - | McpServerOauthLoginRequest - | ConfigMcpServerReloadRequest - | McpServerStatusListRequest - | WindowsSandboxSetupStartRequest - | AccountLoginStartRequest - | AccountLoginCancelRequest - | AccountLogoutRequest - | AccountRateLimitsReadRequest - | FeedbackUploadRequest - | CommandExecRequest - | CommandExecWriteRequest - | CommandExecTerminateRequest - | CommandExecResizeRequest - | ConfigReadRequest - | ExternalAgentConfigDetectRequest - | ExternalAgentConfigImportRequest - | ConfigValueWriteRequest - | ConfigBatchWriteRequest - | ConfigRequirementsReadRequest - | AccountReadRequest - | FuzzyFileSearchRequest, - Field( - description="Request from the client to the server.", title="ClientRequest" - ), - ] - - class Config(BaseModel): model_config = ConfigDict( extra="allow", @@ -6310,6 +7041,98 @@ class ConfigReadResponse(BaseModel): origins: dict[str, ConfigLayerMetadata] +class ExternalAgentConfigDetectResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + items: list[ExternalAgentConfigMigrationItem] + + +class ExternalAgentConfigImportParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + migration_items: Annotated[ + list[ExternalAgentConfigMigrationItem], Field(alias="migrationItems") + ] + + +class RequestPermissionsGuardianApprovalReviewAction(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + permissions: RequestPermissionProfile + reason: str | None = None + type: Annotated[ + Literal["requestPermissions"], + Field(title="RequestPermissionsGuardianApprovalReviewActionType"), + ] + + +class GuardianApprovalReviewAction( + RootModel[ + CommandGuardianApprovalReviewAction + | ExecveGuardianApprovalReviewAction + | ApplyPatchGuardianApprovalReviewAction + | NetworkAccessGuardianApprovalReviewAction + | McpToolCallGuardianApprovalReviewAction + | RequestPermissionsGuardianApprovalReviewAction + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + CommandGuardianApprovalReviewAction + | ExecveGuardianApprovalReviewAction + | ApplyPatchGuardianApprovalReviewAction + | NetworkAccessGuardianApprovalReviewAction + | McpToolCallGuardianApprovalReviewAction + | RequestPermissionsGuardianApprovalReviewAction + ) + + +class ItemGuardianApprovalReviewCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: GuardianApprovalReviewAction + decision_source: Annotated[AutoReviewDecisionSource, Field(alias="decisionSource")] + review: GuardianApprovalReview + review_id: Annotated[ + str, Field(alias="reviewId", description="Stable identifier for this review.") + ] + target_item_id: Annotated[ + str | None, + Field( + alias="targetItemId", + description="Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + ), + ] = None + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] + + +class ItemGuardianApprovalReviewStartedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: GuardianApprovalReviewAction + review: GuardianApprovalReview + review_id: Annotated[ + str, Field(alias="reviewId", description="Stable identifier for this review.") + ] + target_item_id: Annotated[ + str | None, + Field( + alias="targetItemId", + description="Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + ), + ] = None + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] + + class RawResponseItemCompletedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6353,6 +7176,28 @@ class TurnCompletedServerNotification(BaseModel): params: TurnCompletedNotification +class ItemAutoApprovalReviewStartedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["item/autoApprovalReview/started"], + Field(title="Item/autoApprovalReview/startedNotificationMethod"), + ] + params: ItemGuardianApprovalReviewStartedNotification + + +class ItemAutoApprovalReviewCompletedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["item/autoApprovalReview/completed"], + Field(title="Item/autoApprovalReview/completedNotificationMethod"), + ] + params: ItemGuardianApprovalReviewCompletedNotification + + class Thread(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6385,13 +7230,22 @@ class Thread(BaseModel): description="Unix timestamp (in seconds) when the thread was created.", ), ] - cwd: Annotated[str, Field(description="Working directory captured for the thread.")] + cwd: Annotated[ + AbsolutePathBuf, Field(description="Working directory captured for the thread.") + ] ephemeral: Annotated[ bool, Field( description="Whether the thread is ephemeral and should not be materialized on disk." ), ] + forked_from_id: Annotated[ + str | None, + Field( + alias="forkedFromId", + description="Source thread id when this thread was created by forking another thread.", + ), + ] = None git_info: Annotated[ GitInfo | None, Field( @@ -6455,7 +7309,14 @@ class ThreadForkResponse(BaseModel): description="Reviewer currently used for approval requests on this thread.", ), ] - cwd: str + cwd: AbsolutePathBuf + instruction_sources: Annotated[ + list[AbsolutePathBuf] | None, + Field( + alias="instructionSources", + description="Instruction source files currently loaded for this thread.", + ), + ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] reasoning_effort: Annotated[ @@ -6470,6 +7331,13 @@ class ThreadListResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + backwards_cursor: Annotated[ + str | None, + Field( + alias="backwardsCursor", + description="Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one thread. Use it with the opposite `sortDirection`; for timestamp sorts it anchors at the start of the page timestamp so same-second updates are not skipped.", + ), + ] = None data: list[Thread] next_cursor: Annotated[ str | None, @@ -6506,7 +7374,14 @@ class ThreadResumeResponse(BaseModel): description="Reviewer currently used for approval requests on this thread.", ), ] - cwd: str + cwd: AbsolutePathBuf + instruction_sources: Annotated[ + list[AbsolutePathBuf] | None, + Field( + alias="instructionSources", + description="Instruction source files currently loaded for this thread.", + ), + ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] reasoning_effort: Annotated[ @@ -6541,7 +7416,14 @@ class ThreadStartResponse(BaseModel): description="Reviewer currently used for approval requests on this thread.", ), ] - cwd: str + cwd: AbsolutePathBuf + instruction_sources: Annotated[ + list[AbsolutePathBuf] | None, + Field( + alias="instructionSources", + description="Instruction source files currently loaded for this thread.", + ), + ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] reasoning_effort: Annotated[ @@ -6559,6 +7441,27 @@ class ThreadStartedNotification(BaseModel): thread: Thread +class ThreadTurnsListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + backwards_cursor: Annotated[ + str | None, + Field( + alias="backwardsCursor", + description="Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one turn. Use it with the opposite `sortDirection` to include the anchor turn again and catch updates to that turn.", + ), + ] = None + data: list[Turn] + next_cursor: Annotated[ + str | None, + Field( + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last turn. if None, there are no more turns to return.", + ), + ] = None + + class ThreadUnarchiveResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6566,6 +7469,164 @@ class ThreadUnarchiveResponse(BaseModel): thread: Thread +class ExternalAgentConfigImportRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["externalAgentConfig/import"], + Field(title="ExternalAgentConfig/importRequestMethod"), + ] + params: ExternalAgentConfigImportParams + + +class ClientRequest( + RootModel[ + InitializeRequest + | ThreadStartRequest + | ThreadResumeRequest + | ThreadForkRequest + | ThreadArchiveRequest + | ThreadUnsubscribeRequest + | ThreadNameSetRequest + | ThreadMetadataUpdateRequest + | ThreadUnarchiveRequest + | ThreadCompactStartRequest + | ThreadShellCommandRequest + | ThreadRollbackRequest + | ThreadListRequest + | ThreadLoadedListRequest + | ThreadReadRequest + | ThreadTurnsListRequest + | ThreadInjectItemsRequest + | SkillsListRequest + | MarketplaceAddRequest + | MarketplaceRemoveRequest + | PluginListRequest + | PluginReadRequest + | AppListRequest + | FsReadFileRequest + | FsWriteFileRequest + | FsCreateDirectoryRequest + | FsGetMetadataRequest + | FsReadDirectoryRequest + | FsRemoveRequest + | FsCopyRequest + | FsWatchRequest + | FsUnwatchRequest + | SkillsConfigWriteRequest + | PluginInstallRequest + | PluginUninstallRequest + | TurnStartRequest + | TurnSteerRequest + | TurnInterruptRequest + | ReviewStartRequest + | ModelListRequest + | ExperimentalFeatureListRequest + | ExperimentalFeatureEnablementSetRequest + | McpServerOauthLoginRequest + | ConfigMcpServerReloadRequest + | McpServerStatusListRequest + | McpServerResourceReadRequest + | McpServerToolCallRequest + | WindowsSandboxSetupStartRequest + | AccountLoginStartRequest + | AccountLoginCancelRequest + | AccountLogoutRequest + | AccountRateLimitsReadRequest + | AccountSendAddCreditsNudgeEmailRequest + | FeedbackUploadRequest + | CommandExecRequest + | CommandExecWriteRequest + | CommandExecTerminateRequest + | CommandExecResizeRequest + | ConfigReadRequest + | ExternalAgentConfigDetectRequest + | ExternalAgentConfigImportRequest + | ConfigValueWriteRequest + | ConfigBatchWriteRequest + | ConfigRequirementsReadRequest + | AccountReadRequest + | FuzzyFileSearchRequest + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + InitializeRequest + | ThreadStartRequest + | ThreadResumeRequest + | ThreadForkRequest + | ThreadArchiveRequest + | ThreadUnsubscribeRequest + | ThreadNameSetRequest + | ThreadMetadataUpdateRequest + | ThreadUnarchiveRequest + | ThreadCompactStartRequest + | ThreadShellCommandRequest + | ThreadRollbackRequest + | ThreadListRequest + | ThreadLoadedListRequest + | ThreadReadRequest + | ThreadTurnsListRequest + | ThreadInjectItemsRequest + | SkillsListRequest + | MarketplaceAddRequest + | MarketplaceRemoveRequest + | PluginListRequest + | PluginReadRequest + | AppListRequest + | FsReadFileRequest + | FsWriteFileRequest + | FsCreateDirectoryRequest + | FsGetMetadataRequest + | FsReadDirectoryRequest + | FsRemoveRequest + | FsCopyRequest + | FsWatchRequest + | FsUnwatchRequest + | SkillsConfigWriteRequest + | PluginInstallRequest + | PluginUninstallRequest + | TurnStartRequest + | TurnSteerRequest + | TurnInterruptRequest + | ReviewStartRequest + | ModelListRequest + | ExperimentalFeatureListRequest + | ExperimentalFeatureEnablementSetRequest + | McpServerOauthLoginRequest + | ConfigMcpServerReloadRequest + | McpServerStatusListRequest + | McpServerResourceReadRequest + | McpServerToolCallRequest + | WindowsSandboxSetupStartRequest + | AccountLoginStartRequest + | AccountLoginCancelRequest + | AccountLogoutRequest + | AccountRateLimitsReadRequest + | AccountSendAddCreditsNudgeEmailRequest + | FeedbackUploadRequest + | CommandExecRequest + | CommandExecWriteRequest + | CommandExecTerminateRequest + | CommandExecResizeRequest + | ConfigReadRequest + | ExternalAgentConfigDetectRequest + | ExternalAgentConfigImportRequest + | ConfigValueWriteRequest + | ConfigBatchWriteRequest + | ConfigRequirementsReadRequest + | AccountReadRequest + | FuzzyFileSearchRequest, + Field( + description="Request from the client to the server.", title="ClientRequest" + ), + ] + + class ThreadStartedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6603,6 +7664,7 @@ class ServerNotification( | ItemCommandExecutionOutputDeltaServerNotification | ItemCommandExecutionTerminalInteractionServerNotification | ItemFileChangeOutputDeltaServerNotification + | ItemFileChangePatchUpdatedServerNotification | ServerRequestResolvedServerNotification | ItemMcpToolCallProgressServerNotification | McpServerOauthLoginCompletedServerNotification @@ -6610,20 +7672,24 @@ class ServerNotification( | AccountUpdatedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification + | ExternalAgentConfigImportCompletedServerNotification | FsChangedServerNotification | ItemReasoningSummaryTextDeltaServerNotification | ItemReasoningSummaryPartAddedServerNotification | ItemReasoningTextDeltaServerNotification | ThreadCompactedServerNotification | ModelReroutedServerNotification + | WarningServerNotification | DeprecationNoticeServerNotification | ConfigWarningServerNotification | FuzzyFileSearchSessionUpdatedServerNotification | FuzzyFileSearchSessionCompletedServerNotification | ThreadRealtimeStartedServerNotification | ThreadRealtimeItemAddedServerNotification - | ThreadRealtimeTranscriptUpdatedServerNotification + | ThreadRealtimeTranscriptDeltaServerNotification + | ThreadRealtimeTranscriptDoneServerNotification | ThreadRealtimeOutputAudioDeltaServerNotification + | ThreadRealtimeSdpServerNotification | ThreadRealtimeErrorServerNotification | ThreadRealtimeClosedServerNotification | WindowsWorldWritableWarningServerNotification @@ -6660,6 +7726,7 @@ class ServerNotification( | ItemCommandExecutionOutputDeltaServerNotification | ItemCommandExecutionTerminalInteractionServerNotification | ItemFileChangeOutputDeltaServerNotification + | ItemFileChangePatchUpdatedServerNotification | ServerRequestResolvedServerNotification | ItemMcpToolCallProgressServerNotification | McpServerOauthLoginCompletedServerNotification @@ -6667,20 +7734,24 @@ class ServerNotification( | AccountUpdatedServerNotification | AccountRateLimitsUpdatedServerNotification | AppListUpdatedServerNotification + | ExternalAgentConfigImportCompletedServerNotification | FsChangedServerNotification | ItemReasoningSummaryTextDeltaServerNotification | ItemReasoningSummaryPartAddedServerNotification | ItemReasoningTextDeltaServerNotification | ThreadCompactedServerNotification | ModelReroutedServerNotification + | WarningServerNotification | DeprecationNoticeServerNotification | ConfigWarningServerNotification | FuzzyFileSearchSessionUpdatedServerNotification | FuzzyFileSearchSessionCompletedServerNotification | ThreadRealtimeStartedServerNotification | ThreadRealtimeItemAddedServerNotification - | ThreadRealtimeTranscriptUpdatedServerNotification + | ThreadRealtimeTranscriptDeltaServerNotification + | ThreadRealtimeTranscriptDoneServerNotification | ThreadRealtimeOutputAudioDeltaServerNotification + | ThreadRealtimeSdpServerNotification | ThreadRealtimeErrorServerNotification | ThreadRealtimeClosedServerNotification | WindowsWorldWritableWarningServerNotification diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index ce1b847253..68097d4bc6 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -22,7 +22,9 @@ def _assert_no_any_annotations(fn: object) -> None: signature = inspect.signature(fn) for param in signature.parameters.values(): if param.annotation is Any: - raise AssertionError(f"{fn} has public parameter typed as Any: {param.name}") + raise AssertionError( + f"{fn} has public parameter typed as Any: {param.name}" + ) if signature.return_annotation is Any: raise AssertionError(f"{fn} has public return annotation typed as Any") @@ -56,6 +58,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "sandbox", "service_name", "service_tier", + "session_start_source", ], Codex.thread_list: [ "archived", @@ -64,6 +67,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "limit", "model_providers", "search_term", + "sort_direction", "sort_key", "source_kinds", ], @@ -131,6 +135,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "sandbox", "service_name", "service_tier", + "session_start_source", ], AsyncCodex.thread_list: [ "archived", @@ -139,6 +144,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "limit", "model_providers", "search_term", + "sort_direction", "sort_key", "source_kinds", ], @@ -197,7 +203,9 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: for fn, expected_kwargs in expected.items(): actual = _keyword_only_names(fn) assert actual == expected_kwargs, f"unexpected kwargs for {fn}: {actual}" - assert all(name == name.lower() for name in actual), f"non snake_case kwargs in {fn}: {actual}" + assert all(name == name.lower() for name in actual), ( + f"non snake_case kwargs in {fn}: {actual}" + ) _assert_no_any_annotations(fn) @@ -247,4 +255,6 @@ def test_initialize_metadata_requires_non_empty_information() -> None: except RuntimeError as exc: assert "missing required metadata" in str(exc) else: - raise AssertionError("expected RuntimeError when initialize metadata is missing") + raise AssertionError( + "expected RuntimeError when initialize metadata is missing" + )