diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 1a0a69035f..539e9cb8e0 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -47,6 +47,11 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Set up Node.js for js_repl tests + uses: actions/setup-node@v6 + with: + node-version-file: codex-rs/node-version.txt + # Some integration tests rely on DotSlash being installed. # See https://github.com/openai/codex/pull/7617. - name: Install DotSlash @@ -156,6 +161,13 @@ jobs: --build_metadata=VISIBILITY=PUBLIC ) + if [[ "${RUNNER_OS:-}" != "Windows" ]]; then + # Bazel test sandboxes on macOS may resolve an older Homebrew `node` + # before the `actions/setup-node` runtime on PATH. + node_bin="$(which node)" + bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}") + fi + if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then echo "BuildBuddy API key is available; using remote Bazel configuration." # Work around Bazel 9 remote repo contents cache / overlay materialization failures diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 8e8825e9f8..3122a965a2 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -499,6 +499,10 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Set up Node.js for js_repl tests + uses: actions/setup-node@v6 + with: + node-version-file: codex-rs/node-version.txt - name: Install Linux build dependencies if: ${{ runner.os == 'Linux' }} shell: bash diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index e7fc2c5ef2..2d137de6ff 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -178,6 +178,12 @@ jobs: shell: bash run: | set -euo pipefail + # Avoid problematic aws-lc jitter entropy code path on musl builders. + echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV" + target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}" + target_no_jitter="${target_no_jitter//-/_}" + echo "${target_no_jitter}=1" >> "$GITHUB_ENV" + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. echo "RUSTFLAGS=" >> "$GITHUB_ENV" echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" @@ -488,6 +494,10 @@ jobs: --package codex-responses-api-proxy \ --package codex-sdk + - name: Stage macOS and Linux installer script + run: | + cp scripts/install/install.sh dist/install.sh + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 1c6e69d0b6..d3dfd0346e 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -254,8 +254,46 @@ "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository", "attributes": { "deps": { - "rules_rs": "", - "aspect_tools_telemetry": "0.3.2" + "abseil-cpp": "20250814.1", + "apple_support": "1.24.2", + "aspect_bazel_lib": "2.19.3", + "aspect_tools_telemetry": "0.3.2", + "bazel_features": "1.34.0", + "bazel_lib": "3.0.0", + "bazel_skylib": "1.8.2", + "buildozer": "8.2.1", + "gawk": "5.3.2.bcr.1", + "googletest": "1.17.0", + "jq.bzl": "0.1.0", + "jsoncpp": "1.9.6", + "nlohmann_json": "3.6.1", + "openssl": "3.5.4.bcr.0", + "package_metadata": "0.0.5", + "platforms": "1.0.0", + "protobuf": "33.4", + "pybind11_bazel": "2.12.0", + "re2": "2024-07-02.bcr.1", + "rules_android": "0.1.1", + "rules_apple": "4.1.0", + "rules_cc": "0.2.16", + "rules_java": "9.0.3", + "rules_jvm_external": "6.7", + "rules_kotlin": "1.9.6", + "rules_license": "1.0.0", + "rules_perl": "0.5.0", + "rules_pkg": "1.0.1", + "rules_platform": "0.1.0", + "rules_proto": "7.1.0", + "rules_python": "1.7.0", + "rules_rs": "0.0.23", + "rules_shell": "0.6.1", + "rules_swift": "3.1.2", + "stardoc": "0.7.2", + "swift_argument_parser": "1.3.1.2", + "tar.bzl": "0.6.0", + "with_cfg.bzl": "0.12.0", + "yq.bzl": "0.1.1", + "zlib": "1.3.1.bcr.5" } } } @@ -544,13 +582,13 @@ "@@rules_rs+//rs:extensions.bzl%crate": { "Inflector_0.11.4": "{\"dependencies\":[{\"name\":\"lazy_static\",\"optional\":true,\"req\":\"^1.2.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.1\"}],\"features\":{\"default\":[\"heavyweight\"],\"heavyweight\":[\"regex\",\"lazy_static\"],\"unstable\":[]}}", "actix-codec_0.5.2": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.7\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3.7\"},{\"name\":\"memchr\",\"req\":\"^2.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.23.1\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"}],\"features\":{}}", - "actix-http_3.12.0": "{\"dependencies\":[{\"name\":\"actix-codec\",\"req\":\"^0.5\"},{\"features\":[\"openssl\"],\"kind\":\"dev\",\"name\":\"actix-http-test\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"actix-rt\",\"req\":\"^2.2\"},{\"kind\":\"dev\",\"name\":\"actix-server\",\"req\":\"^2\"},{\"name\":\"actix-service\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"actix-tls\",\"optional\":true,\"req\":\"^3.4\"},{\"features\":[\"openssl\",\"rustls-0_23-webpki-roots\"],\"kind\":\"dev\",\"name\":\"actix-tls\",\"req\":\"^3.4\"},{\"name\":\"actix-utils\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"bytestring\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"as_ref\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\"],\"name\":\"derive_more\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.8\"},{\"name\":\"encoding_rs\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.13\"},{\"name\":\"foldhash\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.3.27\"},{\"name\":\"http\",\"req\":\"^0.2.7\"},{\"name\":\"httparse\",\"req\":\"^1.5.1\"},{\"name\":\"httpdate\",\"req\":\"^1.0.1\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"language-tags\",\"req\":\"^0.3\"},{\"name\":\"local-channel\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"memchr\",\"req\":\"^2.4\"},{\"name\":\"mime\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"rustls-pki-types\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tls-openssl\",\"package\":\"openssl\",\"req\":\"^0.10.55\"},{\"kind\":\"dev\",\"name\":\"tls-rustls_023\",\"package\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"features\":[\"net\",\"rt\",\"macros\",\"sync\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"features\":[\"io\",\"codec\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"__compress\":[],\"__tls\":[],\"compress-brotli\":[\"__compress\",\"dep:brotli\"],\"compress-gzip\":[\"__compress\",\"dep:flate2\"],\"compress-zstd\":[\"__compress\",\"dep:zstd\"],\"default\":[],\"http2\":[\"dep:h2\"],\"openssl\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/openssl\"],\"rustls\":[\"__tls\",\"rustls-0_20\"],\"rustls-0_20\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_20\"],\"rustls-0_21\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_21\"],\"rustls-0_22\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_22\"],\"rustls-0_23\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_23\"],\"ws\":[\"dep:local-channel\",\"dep:base64\",\"dep:rand\",\"dep:sha1\"]}}", - "actix-router_0.5.4": "{\"dependencies\":[{\"name\":\"bytestring\",\"req\":\">=0.1.5, <2\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5\"},{\"name\":\"regex-lite\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"}],\"features\":{\"default\":[\"http\",\"unicode\"],\"http\":[\"dep:http\"],\"unicode\":[\"dep:regex\"]}}", + "actix-http_3.11.2": "{\"dependencies\":[{\"name\":\"actix-codec\",\"req\":\"^0.5\"},{\"features\":[\"openssl\"],\"kind\":\"dev\",\"name\":\"actix-http-test\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"actix-rt\",\"req\":\"^2.2\"},{\"kind\":\"dev\",\"name\":\"actix-server\",\"req\":\"^2\"},{\"name\":\"actix-service\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"actix-tls\",\"optional\":true,\"req\":\"^3.4\"},{\"features\":[\"openssl\",\"rustls-0_23-webpki-roots\"],\"kind\":\"dev\",\"name\":\"actix-tls\",\"req\":\"^3.4\"},{\"name\":\"actix-utils\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"bytestring\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"as_ref\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\"],\"name\":\"derive_more\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.8\"},{\"name\":\"encoding_rs\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.13\"},{\"name\":\"foldhash\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.3.27\"},{\"name\":\"http\",\"req\":\"^0.2.7\"},{\"name\":\"httparse\",\"req\":\"^1.5.1\"},{\"name\":\"httpdate\",\"req\":\"^1.0.1\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"language-tags\",\"req\":\"^0.3\"},{\"name\":\"local-channel\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"memchr\",\"req\":\"^2.4\"},{\"name\":\"mime\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tls-openssl\",\"package\":\"openssl\",\"req\":\"^0.10.55\"},{\"kind\":\"dev\",\"name\":\"tls-rustls_023\",\"package\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"features\":[\"net\",\"rt\",\"macros\",\"sync\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"features\":[\"io\",\"codec\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"__compress\":[],\"__tls\":[],\"compress-brotli\":[\"__compress\",\"dep:brotli\"],\"compress-gzip\":[\"__compress\",\"dep:flate2\"],\"compress-zstd\":[\"__compress\",\"dep:zstd\"],\"default\":[],\"http2\":[\"dep:h2\"],\"openssl\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/openssl\"],\"rustls\":[\"__tls\",\"rustls-0_20\"],\"rustls-0_20\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_20\"],\"rustls-0_21\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_21\"],\"rustls-0_22\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_22\"],\"rustls-0_23\":[\"__tls\",\"actix-tls/accept\",\"actix-tls/rustls-0_23\"],\"ws\":[\"dep:local-channel\",\"dep:base64\",\"dep:rand\",\"dep:sha1\"]}}", + "actix-router_0.5.3": "{\"dependencies\":[{\"name\":\"bytestring\",\"req\":\">=0.1.5, <2\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5\"},{\"name\":\"regex-lite\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"}],\"features\":{\"default\":[\"http\",\"unicode\"],\"http\":[\"dep:http\"],\"unicode\":[\"dep:regex\"]}}", "actix-rt_2.11.0": "{\"dependencies\":[{\"name\":\"actix-macros\",\"optional\":true,\"req\":\"^0.2.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"features\":[\"rt\",\"io-util\",\"net\",\"parking_lot\",\"signal\",\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.44.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44.2\"},{\"name\":\"tokio-uring\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"linux\\\")\"}],\"features\":{\"default\":[\"macros\"],\"io-uring\":[\"tokio-uring\"],\"macros\":[\"actix-macros\"]}}", "actix-server_2.6.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-codec\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"actix-rt\",\"req\":\"^2.10\"},{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2.8\"},{\"name\":\"actix-service\",\"req\":\"^2\"},{\"name\":\"actix-utils\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"sink\",\"async-await-macro\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"features\":[\"os-poll\",\"net\"],\"name\":\"mio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"name\":\"socket2\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.44.2\"},{\"features\":[\"io-util\",\"rt-multi-thread\",\"macros\",\"fs\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44.2\"},{\"name\":\"tokio-uring\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"features\":[\"fmt\",\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"default\":[],\"io-uring\":[\"tokio-uring\",\"actix-rt/io-uring\"]}}", "actix-service_2.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"actix-utils\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"}],\"features\":{}}", "actix-utils_3.0.1": "{\"dependencies\":[{\"name\":\"local-waker\",\"req\":\"^0.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"}],\"features\":{}}", - "actix-web_4.13.0": "{\"dependencies\":[{\"name\":\"actix-codec\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"name\":\"actix-http\",\"req\":\"^3.12.0\"},{\"name\":\"actix-macros\",\"optional\":true,\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"http\"],\"name\":\"actix-router\",\"req\":\"^0.5.4\"},{\"default_features\":false,\"name\":\"actix-rt\",\"req\":\"^2.6\"},{\"name\":\"actix-server\",\"req\":\"^2.6\"},{\"name\":\"actix-service\",\"req\":\"^2\"},{\"features\":[\"openssl\",\"rustls-0_23\"],\"kind\":\"dev\",\"name\":\"actix-test\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"actix-tls\",\"optional\":true,\"req\":\"^3.4\"},{\"name\":\"actix-utils\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"actix-web-codegen\",\"optional\":true,\"req\":\"^4.3\"},{\"features\":[\"openssl\"],\"kind\":\"dev\",\"name\":\"awc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"bytestring\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"const-str\",\"req\":\"^0.5\"},{\"features\":[\"percent-encode\"],\"name\":\"cookie\",\"optional\":true,\"req\":\"^0.16\"},{\"kind\":\"dev\",\"name\":\"core_affinity\",\"req\":\"^0.8\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"as_ref\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\"],\"name\":\"derive_more\",\"req\":\"^2\"},{\"name\":\"encoding_rs\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\"},{\"name\":\"foldhash\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"name\":\"impl-more\",\"req\":\"^0.1.4\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"language-tags\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"mime\",\"req\":\"^0.3\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"name\":\"regex-lite\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"rustls-pki-types\",\"req\":\"^1.13.1\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"socket2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tls-openssl\",\"package\":\"openssl\",\"req\":\"^0.10.55\"},{\"kind\":\"dev\",\"name\":\"tls-rustls\",\"package\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"name\":\"url\",\"req\":\"^2.5.4\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"__compress\":[],\"__tls\":[],\"compat\":[\"compat-routing-macros-force-pub\"],\"compat-routing-macros-force-pub\":[\"actix-web-codegen?/compat-routing-macros-force-pub\"],\"compress-brotli\":[\"actix-http/compress-brotli\",\"__compress\"],\"compress-gzip\":[\"actix-http/compress-gzip\",\"__compress\"],\"compress-zstd\":[\"actix-http/compress-zstd\",\"__compress\"],\"cookies\":[\"dep:cookie\"],\"default\":[\"macros\",\"compress-brotli\",\"compress-gzip\",\"compress-zstd\",\"cookies\",\"http2\",\"unicode\",\"compat\",\"ws\"],\"experimental-introspection\":[\"serde/derive\"],\"experimental-io-uring\":[\"actix-server/io-uring\"],\"http2\":[\"actix-http/http2\"],\"macros\":[\"dep:actix-macros\",\"dep:actix-web-codegen\"],\"openssl\":[\"__tls\",\"http2\",\"actix-http/openssl\",\"actix-tls/accept\",\"actix-tls/openssl\"],\"rustls\":[\"rustls-0_20\"],\"rustls-0_20\":[\"__tls\",\"http2\",\"actix-http/rustls-0_20\",\"actix-tls/accept\",\"actix-tls/rustls-0_20\"],\"rustls-0_21\":[\"__tls\",\"http2\",\"actix-http/rustls-0_21\",\"actix-tls/accept\",\"actix-tls/rustls-0_21\"],\"rustls-0_22\":[\"__tls\",\"http2\",\"actix-http/rustls-0_22\",\"actix-tls/accept\",\"actix-tls/rustls-0_22\"],\"rustls-0_23\":[\"__tls\",\"http2\",\"actix-http/rustls-0_23\",\"actix-tls/accept\",\"actix-tls/rustls-0_23\"],\"secure-cookies\":[\"cookies\",\"cookie/secure\"],\"unicode\":[\"dep:regex\",\"actix-router/unicode\"],\"ws\":[\"actix-http/ws\"]}}", + "actix-web_4.12.1": "{\"dependencies\":[{\"name\":\"actix-codec\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"name\":\"actix-http\",\"req\":\"^3.11.2\"},{\"name\":\"actix-macros\",\"optional\":true,\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"http\"],\"name\":\"actix-router\",\"req\":\"^0.5.3\"},{\"default_features\":false,\"name\":\"actix-rt\",\"req\":\"^2.6\"},{\"name\":\"actix-server\",\"req\":\"^2.6\"},{\"name\":\"actix-service\",\"req\":\"^2\"},{\"features\":[\"openssl\",\"rustls-0_23\"],\"kind\":\"dev\",\"name\":\"actix-test\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"actix-tls\",\"optional\":true,\"req\":\"^3.4\"},{\"name\":\"actix-utils\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"actix-web-codegen\",\"optional\":true,\"req\":\"^4.3\"},{\"features\":[\"openssl\"],\"kind\":\"dev\",\"name\":\"awc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"bytestring\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"const-str\",\"req\":\"^0.5\"},{\"features\":[\"percent-encode\"],\"name\":\"cookie\",\"optional\":true,\"req\":\"^0.16\"},{\"kind\":\"dev\",\"name\":\"core_affinity\",\"req\":\"^0.8\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"as_ref\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\"],\"name\":\"derive_more\",\"req\":\"^2\"},{\"name\":\"encoding_rs\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\"},{\"name\":\"foldhash\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"name\":\"impl-more\",\"req\":\"^0.1.4\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"language-tags\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"mime\",\"req\":\"^0.3\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"name\":\"regex-lite\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"socket2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"formatting\"],\"name\":\"time\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tls-openssl\",\"package\":\"openssl\",\"req\":\"^0.10.55\"},{\"kind\":\"dev\",\"name\":\"tls-rustls\",\"package\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"name\":\"url\",\"req\":\"^2.5.4\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"__compress\":[],\"__tls\":[],\"compat\":[\"compat-routing-macros-force-pub\"],\"compat-routing-macros-force-pub\":[\"actix-web-codegen?/compat-routing-macros-force-pub\"],\"compress-brotli\":[\"actix-http/compress-brotli\",\"__compress\"],\"compress-gzip\":[\"actix-http/compress-gzip\",\"__compress\"],\"compress-zstd\":[\"actix-http/compress-zstd\",\"__compress\"],\"cookies\":[\"dep:cookie\"],\"default\":[\"macros\",\"compress-brotli\",\"compress-gzip\",\"compress-zstd\",\"cookies\",\"http2\",\"unicode\",\"compat\",\"ws\"],\"experimental-io-uring\":[\"actix-server/io-uring\"],\"http2\":[\"actix-http/http2\"],\"macros\":[\"dep:actix-macros\",\"dep:actix-web-codegen\"],\"openssl\":[\"__tls\",\"http2\",\"actix-http/openssl\",\"actix-tls/accept\",\"actix-tls/openssl\"],\"rustls\":[\"rustls-0_20\"],\"rustls-0_20\":[\"__tls\",\"http2\",\"actix-http/rustls-0_20\",\"actix-tls/accept\",\"actix-tls/rustls-0_20\"],\"rustls-0_21\":[\"__tls\",\"http2\",\"actix-http/rustls-0_21\",\"actix-tls/accept\",\"actix-tls/rustls-0_21\"],\"rustls-0_22\":[\"__tls\",\"http2\",\"actix-http/rustls-0_22\",\"actix-tls/accept\",\"actix-tls/rustls-0_22\"],\"rustls-0_23\":[\"__tls\",\"http2\",\"actix-http/rustls-0_23\",\"actix-tls/accept\",\"actix-tls/rustls-0_23\"],\"secure-cookies\":[\"cookies\",\"cookie/secure\"],\"unicode\":[\"dep:regex\",\"actix-router/unicode\"],\"ws\":[\"actix-http/ws\"]}}", "addr2line_0.25.1": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"backtrace\",\"req\":\"^0.3.13\"},{\"features\":[\"wrap_help\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.3.21\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"findshlibs\",\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"read\"],\"name\":\"gimli\",\"req\":\"^0.32.0\"},{\"kind\":\"dev\",\"name\":\"libtest-mimic\",\"req\":\"^0.8.1\"},{\"name\":\"memmap2\",\"optional\":true,\"req\":\"^0.9.4\"},{\"default_features\":false,\"features\":[\"read\",\"compression\"],\"name\":\"object\",\"optional\":true,\"req\":\"^0.37.0\"},{\"name\":\"rustc-demangle\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"typed-arena\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"all\":[\"bin\",\"wasm\"],\"bin\":[\"loader\",\"rustc-demangle\",\"cpp_demangle\",\"fallible-iterator\",\"smallvec\",\"dep:clap\"],\"cargo-all\":[],\"default\":[\"rustc-demangle\",\"cpp_demangle\",\"loader\",\"fallible-iterator\",\"smallvec\"],\"loader\":[\"std\",\"dep:object\",\"dep:memmap2\",\"dep:typed-arena\"],\"rustc-dep-of-std\":[\"core\",\"alloc\",\"gimli/rustc-dep-of-std\"],\"std\":[\"gimli/std\"],\"wasm\":[\"object/wasm\"]}}", "adler2_2.0.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[]}}", "aead_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14\"},{\"default_features\":false,\"name\":\"heapless\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"alloc\":[],\"default\":[\"rand_core\"],\"dev\":[\"blobby\"],\"getrandom\":[\"crypto-common/getrandom\",\"rand_core\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\"],\"stream\":[]}}", @@ -572,13 +610,17 @@ "anstyle-query_1.1.5": "{\"dependencies\":[{\"features\":[\"Win32_System_Console\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.60.2, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "anstyle-wincon_3.0.11": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"name\":\"once_cell_polyfill\",\"req\":\"^1.56.1\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_System_Console\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.60.2, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "anstyle_1.0.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", - "anyhow_1.0.102": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"backtrace\":[],\"default\":[\"std\"],\"std\":[]}}", + "anyhow_1.0.101": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.51\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}", "arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}", "arc-swap_1.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}", "arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}", "ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "askama_0.15.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"askama_macros\",\"optional\":true,\"req\":\"=0.15.4\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"itoa\",\"req\":\"^1.0.11\"},{\"default_features\":false,\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[\"askama_macros?/alloc\",\"serde?/alloc\",\"serde_json?/alloc\",\"percent-encoding?/alloc\"],\"code-in-doc\":[\"askama_macros?/code-in-doc\"],\"config\":[\"askama_macros?/config\"],\"default\":[\"config\",\"derive\",\"std\",\"urlencode\"],\"derive\":[\"dep:askama_macros\",\"dep:askama_macros\"],\"full\":[\"default\",\"code-in-doc\",\"serde_json\"],\"nightly-spans\":[\"askama_macros/nightly-spans\"],\"serde_json\":[\"std\",\"askama_macros?/serde_json\",\"dep:serde\",\"dep:serde_json\"],\"std\":[\"alloc\",\"askama_macros?/std\",\"serde?/std\",\"serde_json?/std\",\"percent-encoding?/std\"],\"urlencode\":[\"askama_macros?/urlencode\",\"dep:percent-encoding\"]}}", + "askama_derive_0.15.4": "{\"dependencies\":[{\"name\":\"basic-toml\",\"optional\":true,\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"console\",\"req\":\"^0.16.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2\"},{\"name\":\"parser\",\"package\":\"askama_parser\",\"req\":\"=0.15.4\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.20\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.0\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"rustc-hash\",\"req\":\"^2.0.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.6.0\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"derive\",\"full\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.3\"}],\"features\":{\"alloc\":[],\"code-in-doc\":[\"dep:pulldown-cmark\"],\"config\":[\"external-sources\",\"dep:basic-toml\",\"dep:serde\",\"dep:serde_derive\",\"parser/config\"],\"default\":[\"alloc\",\"code-in-doc\",\"config\",\"external-sources\",\"proc-macro\",\"serde_json\",\"std\",\"urlencode\"],\"external-sources\":[],\"nightly-spans\":[],\"proc-macro\":[\"proc-macro2/proc-macro\"],\"serde_json\":[],\"std\":[\"alloc\"],\"urlencode\":[]}}", + "askama_macros_0.15.4": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"external-sources\",\"proc-macro\"],\"name\":\"askama_derive\",\"package\":\"askama_derive\",\"req\":\"=0.15.4\"}],\"features\":{\"alloc\":[\"askama_derive/alloc\"],\"code-in-doc\":[\"askama_derive/code-in-doc\"],\"config\":[\"askama_derive/config\"],\"default\":[\"config\",\"derive\",\"std\",\"urlencode\"],\"derive\":[],\"full\":[\"default\",\"code-in-doc\",\"serde_json\"],\"nightly-spans\":[\"askama_derive/nightly-spans\"],\"serde_json\":[\"askama_derive/serde_json\"],\"std\":[\"askama_derive/std\"],\"urlencode\":[\"askama_derive/urlencode\"]}}", + "askama_parser_0.15.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"rustc-hash\",\"req\":\"^2.0.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"unicode-ident\",\"req\":\"^1.0.12\"},{\"features\":[\"simd\"],\"name\":\"winnow\",\"req\":\"^0.7.0\"}],\"features\":{\"config\":[\"dep:serde\",\"dep:serde_derive\"]}}", "asn1-rs-derive_0.6.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"synstructure\",\"req\":\"^0.13\"}],\"features\":{}}", "asn1-rs-impl_0.2.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "asn1-rs_0.7.1": "{\"dependencies\":[{\"name\":\"asn1-rs-derive\",\"req\":\"^0.6\"},{\"name\":\"asn1-rs-impl\",\"req\":\"^0.2\"},{\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"colored\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"colored\",\"req\":\"^3.0\"},{\"name\":\"cookie-factory\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"displaydoc\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"nom\",\"req\":\"^7.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"num-traits\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"pem\",\"req\":\"^3.0\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"macros\",\"parsing\",\"formatting\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"bigint\":[\"num-bigint\"],\"bits\":[\"bitvec\"],\"datetime\":[\"time\"],\"debug\":[\"std\",\"colored\"],\"default\":[\"std\"],\"serialize\":[\"cookie-factory\"],\"std\":[],\"trace\":[\"debug\"]}}", @@ -587,7 +629,7 @@ "assert_matches_1.5.0": "{\"dependencies\":[],\"features\":{}}", "async-broadcast_0.7.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.2.0\"},{\"name\":\"event-listener\",\"req\":\"^5.0.0\"},{\"name\":\"event-listener-strategy\",\"req\":\"^0.5.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.21\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^1.11.3\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.21\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.13\"}],\"features\":{}}", "async-channel_2.5.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"concurrent-queue\",\"req\":\"^2.5\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"event-listener-strategy\",\"req\":\"^0.5.4\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"portable-atomic\":[\"concurrent-queue/portable-atomic\",\"event-listener-strategy/portable-atomic\",\"dep:portable-atomic-util\",\"dep:portable-atomic\"],\"std\":[\"concurrent-queue/std\",\"event-listener-strategy/std\"]}}", - "async-executor_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"async-lock\",\"req\":\"^3.0.0\"},{\"name\":\"async-task\",\"req\":\"^4.4.0\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-lite\",\"req\":\"^2.0.0\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"slab\",\"req\":\"^0.4.7\"}],\"features\":{\"static\":[]}}", + "async-executor_1.13.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"async-lock\",\"req\":\"^3.0.0\"},{\"name\":\"async-task\",\"req\":\"^4.4.0\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-lite\",\"req\":\"^2.0.0\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"slab\",\"req\":\"^0.4.7\"}],\"features\":{\"static\":[]}}", "async-fs_2.2.0": "{\"dependencies\":[{\"name\":\"async-lock\",\"req\":\"^3.0.0\"},{\"name\":\"blocking\",\"req\":\"^1.3.0\"},{\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.78\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Storage_FileSystem\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "async-io_2.6.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-net\",\"req\":\"^2.0.0\"},{\"kind\":\"build\",\"name\":\"autocfg\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"blocking\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"inotify\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"parking\",\"req\":\"^2.0.0\"},{\"name\":\"polling\",\"req\":\"^3.4.0\"},{\"default_features\":false,\"features\":[\"fs\",\"net\",\"std\"],\"name\":\"rustix\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"timerfd\",\"req\":\"^1\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"kind\":\"dev\",\"name\":\"uds_windows\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "async-lock_3.4.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"event-listener\",\"req\":\"^5.0.0\"},{\"default_features\":false,\"name\":\"event-listener-strategy\",\"req\":\"^0.5.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"flume\",\"req\":\"^0.12.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"event-listener/loom\",\"dep:loom\"],\"std\":[\"event-listener/std\",\"event-listener-strategy/std\"]}}", @@ -602,7 +644,7 @@ "atoi_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.14\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"num-traits/std\"]}}", "atomic-waker_1.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7.0\"}],\"features\":{}}", "autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}", - "aws-lc-rs_1.16.0": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.37.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", + "aws-lc-rs_1.15.4": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.37.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", "aws-lc-sys_0.37.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", "axum-core_0.5.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", "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\"]}}", @@ -618,7 +660,7 @@ "bit-set_0.5.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bit-vec\",\"req\":\"^0.6.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"bit-vec/std\"]}}", "bit-vec_0.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde_no_std\":[\"serde/alloc\"],\"serde_std\":[\"std\",\"serde/std\"],\"std\":[]}}", "bitflags_1.3.2": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3\"}],\"features\":{\"default\":[],\"example_generated\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\"]}}", - "bitflags_2.11.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", + "bitflags_2.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", "block-buffer_0.10.4": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{}}", "block-padding_0.3.3": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{\"std\":[]}}", "block2_0.6.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"objc2\",\"req\":\">=0.6.2, <0.8.0\"}],\"features\":{\"alloc\":[],\"compiler-rt\":[\"objc2/unstable-compiler-rt\"],\"default\":[\"std\"],\"gnustep-1-7\":[\"objc2/gnustep-1-7\"],\"gnustep-1-8\":[\"gnustep-1-7\",\"objc2/gnustep-1-8\"],\"gnustep-1-9\":[\"gnustep-1-8\",\"objc2/gnustep-1-9\"],\"gnustep-2-0\":[\"gnustep-1-9\",\"objc2/gnustep-2-0\"],\"gnustep-2-1\":[\"gnustep-2-0\",\"objc2/gnustep-2-1\"],\"std\":[\"alloc\"],\"unstable-coerce-pointee\":[],\"unstable-objfw\":[],\"unstable-private\":[],\"unstable-winobjc\":[\"gnustep-1-8\"]}}", @@ -626,7 +668,7 @@ "bm25_2.3.2": "{\"dependencies\":[{\"name\":\"cached\",\"optional\":true,\"req\":\"^0.56.0\"},{\"kind\":\"dev\",\"name\":\"csv\",\"req\":\"^1.3.1\"},{\"name\":\"deunicode\",\"optional\":true,\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.21\"},{\"name\":\"fxhash\",\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.41.1\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.11.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.11.0\"},{\"name\":\"rust-stemmers\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"nltk\"],\"name\":\"stop-words\",\"optional\":true,\"req\":\"^0.9.0\"},{\"name\":\"unicode-segmentation\",\"optional\":true,\"req\":\"^1.12.0\"},{\"name\":\"whichlang\",\"optional\":true,\"req\":\"^0.1.1\"}],\"features\":{\"default\":[\"default_tokenizer\"],\"default_tokenizer\":[\"dep:cached\",\"dep:stop-words\",\"dep:rust-stemmers\",\"dep:deunicode\",\"dep:unicode-segmentation\"],\"language_detection\":[\"dep:whichlang\",\"default_tokenizer\"],\"parallelism\":[\"dep:rayon\"]}}", "borsh_1.6.0": "{\"dependencies\":[{\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.1\"},{\"name\":\"borsh-derive\",\"optional\":true,\"req\":\"~1.6.0\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2.1\"},{\"name\":\"hashbrown\",\"optional\":true,\"req\":\">=0.11, <0.16.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.29.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"de_strict_order\":[],\"default\":[\"std\"],\"derive\":[\"borsh-derive\"],\"rc\":[],\"std\":[],\"unstable__schema\":[\"derive\",\"borsh-derive/schema\"]}}", "bstr_1.12.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.7.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"dfa-search\"],\"name\":\"regex-automata\",\"optional\":true,\"req\":\"^0.4.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.85\"},{\"kind\":\"dev\",\"name\":\"ucd-parse\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"unicode-segmentation\",\"req\":\"^1.2.1\"}],\"features\":{\"alloc\":[\"memchr/alloc\",\"serde?/alloc\"],\"default\":[\"std\",\"unicode\"],\"serde\":[\"dep:serde\"],\"std\":[\"alloc\",\"memchr/std\",\"serde?/std\"],\"unicode\":[\"dep:regex-automata\"]}}", - "bumpalo_3.20.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"blink-alloc\",\"req\":\"=0.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"=1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"=1.10.0\"},{\"kind\":\"dev\",\"name\":\"rayon-core\",\"req\":\"=1.12.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.171\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.197\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.115\"}],\"features\":{\"allocator_api\":[],\"bench_allocator_api\":[\"allocator_api\",\"blink-alloc/nightly\"],\"boxed\":[],\"collections\":[],\"default\":[],\"serde\":[\"dep:serde\"],\"std\":[]}}", + "bumpalo_3.19.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"blink-alloc\",\"req\":\"=0.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"=1.10.0\"},{\"kind\":\"dev\",\"name\":\"rayon-core\",\"req\":\"=1.12.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.171\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.197\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.115\"}],\"features\":{\"allocator_api\":[],\"bench_allocator_api\":[\"allocator_api\",\"blink-alloc/nightly\"],\"boxed\":[],\"collections\":[],\"default\":[],\"serde\":[\"dep:serde\"],\"std\":[]}}", "bytemuck_1.25.0": "{\"dependencies\":[{\"name\":\"bytemuck_derive\",\"optional\":true,\"req\":\"^1.10.2\"},{\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.22\"}],\"features\":{\"aarch64_simd\":[],\"align_offset\":[],\"alloc_uninit\":[],\"avx512_simd\":[],\"const_zeroed\":[],\"derive\":[\"bytemuck_derive\"],\"extern_crate_alloc\":[],\"extern_crate_std\":[\"extern_crate_alloc\"],\"impl_core_error\":[],\"latest_stable_rust\":[\"aarch64_simd\",\"avx512_simd\",\"align_offset\",\"alloc_uninit\",\"const_zeroed\",\"derive\",\"impl_core_error\",\"min_const_generics\",\"must_cast\",\"must_cast_extra\",\"pod_saturating\",\"track_caller\",\"transparentwrapper_extra\",\"wasm_simd\",\"zeroable_atomics\",\"zeroable_maybe_uninit\",\"zeroable_unwind_fn\"],\"min_const_generics\":[],\"must_cast\":[],\"must_cast_extra\":[\"must_cast\"],\"nightly_docs\":[],\"nightly_float\":[],\"nightly_portable_simd\":[\"rustversion\"],\"nightly_stdsimd\":[],\"pod_saturating\":[],\"track_caller\":[],\"transparentwrapper_extra\":[],\"unsound_ptr_pod_impl\":[],\"wasm_simd\":[],\"zeroable_atomics\":[],\"zeroable_maybe_uninit\":[],\"zeroable_unwind_fn\":[]}}", "byteorder-lite_0.1.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "byteorder_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"std\"],\"i128\":[],\"std\":[]}}", @@ -640,7 +682,7 @@ "cassowary_0.3.0": "{\"dependencies\":[],\"features\":{}}", "castaway_0.2.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "cbc_0.1.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aes\",\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"alloc\":[\"cipher/alloc\"],\"block-padding\":[\"cipher/block-padding\"],\"default\":[\"block-padding\"],\"std\":[\"cipher/std\",\"alloc\"],\"zeroize\":[\"cipher/zeroize\"]}}", - "cc_1.2.56": "{\"dependencies\":[{\"name\":\"find-msvc-tools\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"jobserver\",\"optional\":true,\"req\":\"^0.1.30\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.62\",\"target\":\"cfg(unix)\"},{\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"jobserver\":[],\"parallel\":[\"dep:libc\",\"dep:jobserver\"]}}", + "cc_1.2.55": "{\"dependencies\":[{\"name\":\"find-msvc-tools\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"jobserver\",\"optional\":true,\"req\":\"^0.1.30\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.62\",\"target\":\"cfg(unix)\"},{\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"jobserver\":[],\"parallel\":[\"dep:libc\",\"dep:jobserver\"]}}", "cesu8_1.1.0": "{\"dependencies\":[],\"features\":{\"unstable\":[]}}", "cexpr_0.6.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"clang-sys\",\"req\":\">=0.13.0, <0.29.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"nom\",\"req\":\"^7\"}],\"features\":{}}", "cfg-if_1.0.4": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"rustc-dep-of-std\":[\"core\"]}}", @@ -649,13 +691,13 @@ "chacha20_0.9.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"std\":[\"cipher/std\"],\"zeroize\":[\"cipher/zeroize\"]}}", "chacha20poly1305_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"req\":\"^0.9\"},{\"name\":\"cipher\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"reduced-round\":[],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "chardetng_0.1.17": "{\"dependencies\":[{\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"detone\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"encoding_rs\",\"req\":\"^0.8.29\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.2.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"multithreading\":[\"rayon\",\"arrayvec\"],\"testing-only-no-semver-guarantees-do-not-use\":[]}}", - "chrono_0.4.44": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.0\"},{\"name\":\"defmt\",\"optional\":true,\"req\":\"^1.0.1\"},{\"features\":[\"fallback\"],\"name\":\"iana-time-zone\",\"optional\":true,\"req\":\"^0.1.45\",\"target\":\"cfg(unix)\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"pure-rust-locales\",\"optional\":true,\"req\":\"^0.8.2\"},{\"default_features\":false,\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.43\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.99\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.66\"},{\"name\":\"windows-link\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_bench\":[],\"alloc\":[],\"clock\":[\"winapi\",\"iana-time-zone\",\"now\"],\"core-error\":[],\"default\":[\"clock\",\"std\",\"oldtime\",\"wasmbind\"],\"defmt\":[\"dep:defmt\",\"pure-rust-locales?/defmt\"],\"libc\":[],\"now\":[\"std\"],\"oldtime\":[],\"rkyv\":[\"dep:rkyv\",\"rkyv/size_32\"],\"rkyv-16\":[\"dep:rkyv\",\"rkyv?/size_16\"],\"rkyv-32\":[\"dep:rkyv\",\"rkyv?/size_32\"],\"rkyv-64\":[\"dep:rkyv\",\"rkyv?/size_64\"],\"rkyv-validation\":[\"rkyv?/validation\"],\"std\":[\"alloc\"],\"unstable-locales\":[\"pure-rust-locales\"],\"wasmbind\":[\"wasm-bindgen\",\"js-sys\"],\"winapi\":[\"windows-link\"]}}", + "chrono_0.4.43": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.0\"},{\"name\":\"defmt\",\"optional\":true,\"req\":\"^1.0.1\"},{\"features\":[\"fallback\"],\"name\":\"iana-time-zone\",\"optional\":true,\"req\":\"^0.1.45\",\"target\":\"cfg(unix)\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"pure-rust-locales\",\"optional\":true,\"req\":\"^0.8.2\"},{\"default_features\":false,\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.43\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.99\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.66\"},{\"name\":\"windows-link\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_bench\":[],\"alloc\":[],\"clock\":[\"winapi\",\"iana-time-zone\",\"now\"],\"core-error\":[],\"default\":[\"clock\",\"std\",\"oldtime\",\"wasmbind\"],\"defmt\":[\"dep:defmt\",\"pure-rust-locales?/defmt\"],\"libc\":[],\"now\":[\"std\"],\"oldtime\":[],\"rkyv\":[\"dep:rkyv\",\"rkyv/size_32\"],\"rkyv-16\":[\"dep:rkyv\",\"rkyv?/size_16\"],\"rkyv-32\":[\"dep:rkyv\",\"rkyv?/size_32\"],\"rkyv-64\":[\"dep:rkyv\",\"rkyv?/size_64\"],\"rkyv-validation\":[\"rkyv?/validation\"],\"std\":[\"alloc\"],\"unstable-locales\":[\"pure-rust-locales\"],\"wasmbind\":[\"wasm-bindgen\",\"js-sys\"],\"winapi\":[\"windows-link\"]}}", "chunked_transfer_1.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"}],\"features\":{}}", "cipher_0.4.4": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.6\"},{\"name\":\"inout\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[],\"block-padding\":[\"inout/block-padding\"],\"dev\":[\"blobby\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\",\"inout/std\"]}}", "clang-sys_1.8.1": "{\"dependencies\":[{\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"build\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.39\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\">=3.0.0, <3.7.0\"}],\"features\":{\"clang_10_0\":[\"clang_9_0\"],\"clang_11_0\":[\"clang_10_0\"],\"clang_12_0\":[\"clang_11_0\"],\"clang_13_0\":[\"clang_12_0\"],\"clang_14_0\":[\"clang_13_0\"],\"clang_15_0\":[\"clang_14_0\"],\"clang_16_0\":[\"clang_15_0\"],\"clang_17_0\":[\"clang_16_0\"],\"clang_18_0\":[\"clang_17_0\"],\"clang_3_5\":[],\"clang_3_6\":[\"clang_3_5\"],\"clang_3_7\":[\"clang_3_6\"],\"clang_3_8\":[\"clang_3_7\"],\"clang_3_9\":[\"clang_3_8\"],\"clang_4_0\":[\"clang_3_9\"],\"clang_5_0\":[\"clang_4_0\"],\"clang_6_0\":[\"clang_5_0\"],\"clang_7_0\":[\"clang_6_0\"],\"clang_8_0\":[\"clang_7_0\"],\"clang_9_0\":[\"clang_8_0\"],\"libcpp\":[],\"runtime\":[\"libloading\"],\"static\":[]}}", - "clap_4.5.60": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"clap-cargo\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"name\":\"clap_builder\",\"req\":\"=4.5.60\"},{\"name\":\"clap_derive\",\"optional\":true,\"req\":\"=4.5.55\"},{\"kind\":\"dev\",\"name\":\"jiff\",\"req\":\"^0.2.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.26\"},{\"kind\":\"dev\",\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"features\":[\"term-svg\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.3\"}],\"features\":{\"cargo\":[\"clap_builder/cargo\"],\"color\":[\"clap_builder/color\"],\"debug\":[\"clap_builder/debug\",\"clap_derive?/debug\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[\"clap_builder/deprecated\",\"clap_derive?/deprecated\"],\"derive\":[\"dep:clap_derive\"],\"env\":[\"clap_builder/env\"],\"error-context\":[\"clap_builder/error-context\"],\"help\":[\"clap_builder/help\"],\"std\":[\"clap_builder/std\"],\"string\":[\"clap_builder/string\"],\"suggestions\":[\"clap_builder/suggestions\"],\"unicode\":[\"clap_builder/unicode\"],\"unstable-derive-ui-tests\":[],\"unstable-doc\":[\"clap_builder/unstable-doc\",\"derive\"],\"unstable-ext\":[\"clap_builder/unstable-ext\"],\"unstable-markdown\":[\"clap_derive/unstable-markdown\"],\"unstable-styles\":[\"clap_builder/unstable-styles\"],\"unstable-v5\":[\"clap_builder/unstable-v5\",\"clap_derive?/unstable-v5\",\"deprecated\"],\"usage\":[\"clap_builder/usage\"],\"wrap_help\":[\"clap_builder/wrap_help\"]}}", - "clap_builder_4.5.60": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.7\"},{\"name\":\"anstyle\",\"req\":\"^1.0.8\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.73\"},{\"name\":\"clap_lex\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"color-print\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"optional\":true,\"req\":\"^0.11.0\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"unic-emoji-char\",\"req\":\"^0.9.0\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.6.0\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"cargo\":[],\"color\":[\"dep:anstream\"],\"debug\":[\"dep:backtrace\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[],\"env\":[],\"error-context\":[],\"help\":[],\"std\":[\"anstyle/std\"],\"string\":[],\"suggestions\":[\"dep:strsim\",\"error-context\"],\"unicode\":[\"dep:unicode-width\",\"dep:unicase\"],\"unstable-doc\":[\"cargo\",\"wrap_help\",\"env\",\"unicode\",\"string\",\"unstable-ext\"],\"unstable-ext\":[],\"unstable-styles\":[\"color\"],\"unstable-v5\":[\"deprecated\"],\"usage\":[],\"wrap_help\":[\"help\",\"dep:terminal_size\"]}}", - "clap_complete_4.5.66": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"default_features\":false,\"features\":[\"std\",\"derive\",\"help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"name\":\"clap_lex\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"completest\",\"optional\":true,\"req\":\"^0.4.2\"},{\"name\":\"completest-pty\",\"optional\":true,\"req\":\"^0.5.5\"},{\"name\":\"is_executable\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"shlex\",\"optional\":true,\"req\":\"^1.3.0\"},{\"features\":[\"diff\",\"dir\",\"examples\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.1\"}],\"features\":{\"debug\":[\"clap/debug\"],\"default\":[],\"unstable-doc\":[\"unstable-dynamic\"],\"unstable-dynamic\":[\"dep:clap_lex\",\"dep:shlex\",\"dep:is_executable\",\"clap/unstable-ext\"],\"unstable-shell-tests\":[\"dep:completest\",\"dep:completest-pty\"]}}", + "clap_4.5.58": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"clap-cargo\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"name\":\"clap_builder\",\"req\":\"=4.5.58\"},{\"name\":\"clap_derive\",\"optional\":true,\"req\":\"=4.5.55\"},{\"kind\":\"dev\",\"name\":\"jiff\",\"req\":\"^0.2.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.26\"},{\"kind\":\"dev\",\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"features\":[\"term-svg\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.3\"}],\"features\":{\"cargo\":[\"clap_builder/cargo\"],\"color\":[\"clap_builder/color\"],\"debug\":[\"clap_builder/debug\",\"clap_derive?/debug\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[\"clap_builder/deprecated\",\"clap_derive?/deprecated\"],\"derive\":[\"dep:clap_derive\"],\"env\":[\"clap_builder/env\"],\"error-context\":[\"clap_builder/error-context\"],\"help\":[\"clap_builder/help\"],\"std\":[\"clap_builder/std\"],\"string\":[\"clap_builder/string\"],\"suggestions\":[\"clap_builder/suggestions\"],\"unicode\":[\"clap_builder/unicode\"],\"unstable-derive-ui-tests\":[],\"unstable-doc\":[\"clap_builder/unstable-doc\",\"derive\"],\"unstable-ext\":[\"clap_builder/unstable-ext\"],\"unstable-markdown\":[\"clap_derive/unstable-markdown\"],\"unstable-styles\":[\"clap_builder/unstable-styles\"],\"unstable-v5\":[\"clap_builder/unstable-v5\",\"clap_derive?/unstable-v5\",\"deprecated\"],\"usage\":[\"clap_builder/usage\"],\"wrap_help\":[\"clap_builder/wrap_help\"]}}", + "clap_builder_4.5.58": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.7\"},{\"name\":\"anstyle\",\"req\":\"^1.0.8\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.73\"},{\"name\":\"clap_lex\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"color-print\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"optional\":true,\"req\":\"^0.11.0\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"unic-emoji-char\",\"req\":\"^0.9.0\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.6.0\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"cargo\":[],\"color\":[\"dep:anstream\"],\"debug\":[\"dep:backtrace\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[],\"env\":[],\"error-context\":[],\"help\":[],\"std\":[\"anstyle/std\"],\"string\":[],\"suggestions\":[\"dep:strsim\",\"error-context\"],\"unicode\":[\"dep:unicode-width\",\"dep:unicase\"],\"unstable-doc\":[\"cargo\",\"wrap_help\",\"env\",\"unicode\",\"string\",\"unstable-ext\"],\"unstable-ext\":[],\"unstable-styles\":[\"color\"],\"unstable-v5\":[\"deprecated\"],\"usage\":[],\"wrap_help\":[\"help\",\"dep:terminal_size\"]}}", + "clap_complete_4.5.65": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"default_features\":false,\"features\":[\"std\",\"derive\",\"help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"name\":\"clap_lex\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"completest\",\"optional\":true,\"req\":\"^0.4.2\"},{\"name\":\"completest-pty\",\"optional\":true,\"req\":\"^0.5.5\"},{\"name\":\"is_executable\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"shlex\",\"optional\":true,\"req\":\"^1.3.0\"},{\"features\":[\"diff\",\"dir\",\"examples\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.1\"}],\"features\":{\"debug\":[\"clap/debug\"],\"default\":[],\"unstable-doc\":[\"unstable-dynamic\"],\"unstable-dynamic\":[\"dep:clap_lex\",\"dep:shlex\",\"dep:is_executable\",\"clap/unstable-ext\"],\"unstable-shell-tests\":[\"dep:completest\",\"dep:completest-pty\"]}}", "clap_derive_4.5.55": "{\"dependencies\":[{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.10\"},{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.69\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.0\"},{\"name\":\"quote\",\"req\":\"^1.0.9\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.8\"}],\"features\":{\"debug\":[],\"default\":[],\"deprecated\":[],\"raw-deprecated\":[\"deprecated\"],\"unstable-markdown\":[\"dep:pulldown-cmark\",\"dep:anstyle\"],\"unstable-v5\":[\"deprecated\"]}}", "clap_lex_1.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"}],\"features\":{}}", "clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}", @@ -723,7 +765,7 @@ "deflate64_0.1.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.7.1\"}],\"features\":{}}", "der-parser_10.0.0": "{\"dependencies\":[{\"name\":\"asn1-rs\",\"req\":\"^0.7\"},{\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"cookie-factory\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.0\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"test-case\",\"req\":\"^3.0\"}],\"features\":{\"as_bitvec\":[\"bitvec\"],\"bigint\":[\"num-bigint\"],\"default\":[\"std\"],\"serialize\":[\"std\",\"cookie-factory\"],\"std\":[],\"unstable\":[]}}", "der_0.7.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9.2\"},{\"name\":\"der_derive\",\"optional\":true,\"req\":\"^0.7.2\"},{\"name\":\"flagset\",\"optional\":true,\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"features\":[\"alloc\"],\"name\":\"pem-rfc7468\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"arbitrary\":[\"dep:arbitrary\",\"const-oid?/arbitrary\",\"std\"],\"bytes\":[\"dep:bytes\",\"alloc\"],\"derive\":[\"dep:der_derive\"],\"oid\":[\"dep:const-oid\"],\"pem\":[\"dep:pem-rfc7468\",\"alloc\",\"zeroize\"],\"real\":[],\"std\":[\"alloc\"]}}", - "deranged_0.5.8": "{\"dependencies\":[{\"name\":\"deranged-macros\",\"optional\":true,\"req\":\"=0.3.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.15\"},{\"default_features\":false,\"name\":\"powerfmt\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"rand010\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"rand010\",\"package\":\"rand\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.86\"}],\"features\":{\"alloc\":[],\"default\":[],\"macros\":[\"dep:deranged-macros\"],\"num\":[\"dep:num-traits\"],\"powerfmt\":[\"dep:powerfmt\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\"],\"rand\":[\"rand08\",\"rand09\",\"rand010\"],\"rand010\":[\"dep:rand010\"],\"rand08\":[\"dep:rand08\"],\"rand09\":[\"dep:rand09\"],\"serde\":[\"dep:serde_core\"]}}", + "deranged_0.5.5": "{\"dependencies\":[{\"name\":\"deranged-macros\",\"optional\":true,\"req\":\"=0.3.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.15\"},{\"default_features\":false,\"name\":\"powerfmt\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.86\"}],\"features\":{\"alloc\":[],\"default\":[],\"macros\":[\"dep:deranged-macros\"],\"num\":[\"dep:num-traits\"],\"powerfmt\":[\"dep:powerfmt\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\"],\"rand09\":[\"dep:rand09\"],\"serde\":[\"dep:serde_core\"]}}", "derivative_2.2.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18, < 1.0.23\"}],\"features\":{\"use_core\":[]}}", "derive_arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"derive\",\"parsing\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "derive_more-impl_1.0.0": "{\"dependencies\":[{\"name\":\"convert_case\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.13.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"syn\",\"req\":\"^2.0.45\"},{\"name\":\"unicode-xid\",\"optional\":true,\"req\":\"^0.2.2\"}],\"features\":{\"add\":[],\"add_assign\":[],\"as_ref\":[\"syn/extra-traits\",\"syn/visit\"],\"constructor\":[],\"debug\":[\"syn/extra-traits\",\"dep:unicode-xid\"],\"default\":[],\"deref\":[],\"deref_mut\":[],\"display\":[\"syn/extra-traits\",\"dep:unicode-xid\"],\"error\":[\"syn/extra-traits\"],\"from\":[\"syn/extra-traits\"],\"from_str\":[],\"full\":[\"add\",\"add_assign\",\"as_ref\",\"constructor\",\"debug\",\"deref\",\"deref_mut\",\"display\",\"error\",\"from\",\"from_str\",\"index\",\"index_mut\",\"into\",\"into_iterator\",\"is_variant\",\"mul\",\"mul_assign\",\"not\",\"sum\",\"try_from\",\"try_into\",\"try_unwrap\",\"unwrap\"],\"index\":[],\"index_mut\":[],\"into\":[\"syn/extra-traits\"],\"into_iterator\":[],\"is_variant\":[\"dep:convert_case\"],\"mul\":[\"syn/extra-traits\"],\"mul_assign\":[\"syn/extra-traits\"],\"not\":[\"syn/extra-traits\"],\"sum\":[],\"testing-helpers\":[\"dep:rustc_version\"],\"try_from\":[],\"try_into\":[\"syn/extra-traits\"],\"try_unwrap\":[\"dep:convert_case\"],\"unwrap\":[\"dep:convert_case\"]}}", @@ -751,7 +793,7 @@ "dupe_derive_0.9.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0.3\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}", "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\"]}}", - "ena_0.14.4": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}", + "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\"]}}", "endi_1.1.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -786,7 +828,7 @@ "fixed_decimal_0.7.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"features\":[\"wasm_js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_distr\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"small\"],\"name\":\"ryu\",\"optional\":true,\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"smallvec\",\"req\":\"^1.10.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"writeable\",\"req\":\"^0.6.0\"}],\"features\":{\"experimental\":[],\"ryu\":[\"dep:ryu\"]}}", "fixedbitset_0.4.2": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "fixedbitset_0.5.7": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", - "flate2_1.1.9": "{\"dependencies\":[{\"name\":\"cloudflare-zlib-sys\",\"optional\":true,\"req\":\"^0.3.6\"},{\"name\":\"crc32fast\",\"optional\":true,\"req\":\"^1.2.0\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"libz-ng-sys\",\"optional\":true,\"req\":\"^1.1.16\"},{\"default_features\":false,\"name\":\"libz-sys\",\"optional\":true,\"req\":\"^1.1.20\"},{\"default_features\":false,\"features\":[\"with-alloc\",\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8.5\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"emscripten\\\")))\"},{\"default_features\":false,\"features\":[\"with-alloc\",\"simd\"],\"name\":\"miniz_oxide\",\"optional\":true,\"req\":\"^0.8.5\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\",\"rust-allocator\"],\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.6.0\"}],\"features\":{\"any_c_zlib\":[\"any_zlib\"],\"any_impl\":[],\"any_zlib\":[\"any_impl\"],\"cloudflare_zlib\":[\"any_c_zlib\",\"cloudflare-zlib-sys\",\"dep:crc32fast\"],\"default\":[\"rust_backend\"],\"miniz-sys\":[\"rust_backend\"],\"miniz_oxide\":[\"any_impl\",\"dep:miniz_oxide\",\"dep:crc32fast\"],\"rust_backend\":[\"miniz_oxide\",\"any_impl\"],\"zlib\":[\"any_c_zlib\",\"libz-sys\",\"dep:crc32fast\"],\"zlib-default\":[\"any_c_zlib\",\"libz-sys/default\",\"dep:crc32fast\"],\"zlib-ng\":[\"any_c_zlib\",\"libz-ng-sys\",\"dep:crc32fast\"],\"zlib-ng-compat\":[\"zlib\",\"libz-sys/zlib-ng\",\"dep:crc32fast\"],\"zlib-rs\":[\"any_zlib\",\"dep:zlib-rs\"]}}", + "flate2_1.1.8": "{\"dependencies\":[{\"name\":\"cloudflare-zlib-sys\",\"optional\":true,\"req\":\"^0.3.6\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"libz-ng-sys\",\"optional\":true,\"req\":\"^1.1.16\"},{\"default_features\":false,\"name\":\"libz-sys\",\"optional\":true,\"req\":\"^1.1.20\"},{\"default_features\":false,\"features\":[\"with-alloc\",\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8.5\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"emscripten\\\")))\"},{\"default_features\":false,\"features\":[\"with-alloc\",\"simd\"],\"name\":\"miniz_oxide\",\"optional\":true,\"req\":\"^0.8.5\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\",\"rust-allocator\"],\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5.5\"}],\"features\":{\"any_c_zlib\":[\"any_zlib\"],\"any_impl\":[],\"any_zlib\":[\"any_impl\"],\"cloudflare_zlib\":[\"any_c_zlib\",\"cloudflare-zlib-sys\"],\"default\":[\"rust_backend\"],\"miniz-sys\":[\"rust_backend\"],\"rust_backend\":[\"miniz_oxide\",\"any_impl\"],\"zlib\":[\"any_c_zlib\",\"libz-sys\"],\"zlib-default\":[\"any_c_zlib\",\"libz-sys/default\"],\"zlib-ng\":[\"any_c_zlib\",\"libz-ng-sys\"],\"zlib-ng-compat\":[\"zlib\",\"libz-sys/zlib-ng\"],\"zlib-rs\":[\"any_zlib\",\"dep:zlib-rs\"]}}", "float-cmp_0.10.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.1\"}],\"features\":{\"default\":[\"ratio\"],\"ratio\":[\"num-traits\"],\"std\":[]}}", "fluent-bundle_0.15.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"fluent-langneg\",\"req\":\"^0.13\"},{\"name\":\"fluent-syntax\",\"req\":\"^0.11.1\"},{\"kind\":\"dev\",\"name\":\"iai\",\"req\":\"^0.1\"},{\"name\":\"intl-memoizer\",\"req\":\"^0.5.2\"},{\"name\":\"intl_pluralrules\",\"req\":\"^7.0.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"rustc-hash\",\"req\":\"^1\"},{\"name\":\"self_cell\",\"req\":\"^0.10\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8\"},{\"name\":\"smallvec\",\"req\":\"^1\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{\"all-benchmarks\":[],\"default\":[]}}", "fluent-langneg_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-locale\",\"req\":\"^0.9\"}],\"features\":{\"cldr\":[\"unic-langid/likelysubtags\"],\"default\":[]}}", @@ -802,17 +844,17 @@ "form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}", "fs_extra_1.3.0": "{\"dependencies\":[],\"features\":{}}", "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", - "futures-channel_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", - "futures-core_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-executor_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.32\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\"]}}", + "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", + "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", + "futures-executor_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"name\":\"num_cpus\",\"optional\":true,\"req\":\"^1.8.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\",\"num_cpus\"]}}", "futures-intrusive_0.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"crossbeam\",\"req\":\"^0.7\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"lock_api\",\"req\":\"^0.4.1\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"kind\":\"dev\",\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.1.11\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.14\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"parking_lot\"]}}", - "futures-io_0.3.32": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", + "futures-io_0.3.31": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", "futures-lite_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"fastrand\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.5\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.5\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.3.3\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"spin_on\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.0.0\"}],\"features\":{\"alloc\":[],\"default\":[\"race\",\"std\"],\"race\":[\"fastrand\"],\"std\":[\"alloc\",\"fastrand/std\",\"futures-io\",\"parking\"]}}", - "futures-macro_0.3.32": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", - "futures-sink_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", - "futures-task_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-util_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.26\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"default_features\":false,\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.7\"},{\"name\":\"spin\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"slab\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\",\"libc\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\",\"libc\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab/std\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", - "futures_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-channel\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-io\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"futures-sink/alloc\",\"futures-channel/alloc\",\"futures-util/alloc\"],\"async-await\":[\"futures-util/async-await\",\"futures-util/async-await-macro\"],\"bilock\":[\"futures-util/bilock\"],\"cfg-target-has-atomic\":[],\"compat\":[\"std\",\"futures-util/compat\"],\"default\":[\"std\",\"async-await\",\"executor\"],\"executor\":[\"std\",\"futures-executor/std\"],\"io-compat\":[\"compat\",\"futures-util/io-compat\"],\"spin\":[\"futures-util/spin\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"futures-io/std\",\"futures-sink/std\",\"futures-util/std\",\"futures-util/io\",\"futures-util/channel\"],\"thread-pool\":[\"executor\",\"futures-executor/thread-pool\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\",\"futures-channel/unstable\",\"futures-io/unstable\",\"futures-util/unstable\"],\"write-all-vectored\":[\"futures-util/write-all-vectored\"]}}", + "futures-macro_0.3.31": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", + "futures-sink_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "futures-task_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", + "futures-util_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", + "futures_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-channel\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-io\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"futures-sink/alloc\",\"futures-channel/alloc\",\"futures-util/alloc\"],\"async-await\":[\"futures-util/async-await\",\"futures-util/async-await-macro\"],\"bilock\":[\"futures-util/bilock\"],\"cfg-target-has-atomic\":[],\"compat\":[\"std\",\"futures-util/compat\"],\"default\":[\"std\",\"async-await\",\"executor\"],\"executor\":[\"std\",\"futures-executor/std\"],\"io-compat\":[\"compat\",\"futures-util/io-compat\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"futures-io/std\",\"futures-sink/std\",\"futures-util/std\",\"futures-util/io\",\"futures-util/channel\"],\"thread-pool\":[\"executor\",\"futures-executor/thread-pool\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\",\"futures-channel/unstable\",\"futures-io/unstable\",\"futures-util/unstable\"],\"write-all-vectored\":[\"futures-util/write-all-vectored\"]}}", "fxhash_0.2.1": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"}],\"features\":{}}", "generator_0.8.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.100\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\">=0.1, <=0.2\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-result\",\"req\":\">=0.3.1, <=0.4\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "generic-array_0.14.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"typenum\",\"req\":\"^1.12\"},{\"kind\":\"build\",\"name\":\"version_check\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"more_lengths\":[]}}", @@ -820,7 +862,6 @@ "getopts_0.2.24": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"std\",\"optional\":true,\"package\":\"rustc-std-workspace-std\",\"req\":\"^1.0\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"default\":[\"unicode\"],\"rustc-dep-of-std\":[\"std\",\"core\"],\"unicode\":[\"dep:unicode-width\"]}}", "getrandom_0.2.17": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"wasi\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.62\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.18\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"}],\"features\":{\"custom\":[],\"js\":[\"wasm-bindgen\",\"js-sys\"],\"linux_disable_fallback\":[],\"rdrand\":[],\"rustc-dep-of-std\":[\"compiler_builtins\",\"core\",\"libc/rustc-dep-of-std\",\"wasi/rustc-dep-of-std\"],\"std\":[],\"test-in-browser\":[]}}", "getrandom_0.3.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), not(any(all(target_os = \\\"linux\\\", target_env = \\\"\\\"), getrandom_backend = \\\"custom\\\", getrandom_backend = \\\"linux_raw\\\", getrandom_backend = \\\"rdrand\\\", getrandom_backend = \\\"rndr\\\"))))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"hurd\\\", target_os = \\\"illumos\\\", target_os = \\\"cygwin\\\", all(target_os = \\\"horizon\\\", target_arch = \\\"arm\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"haiku\\\", target_os = \\\"redox\\\", target_os = \\\"nto\\\", target_os = \\\"aix\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\", target_os = \\\"tvos\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"openbsd\\\", target_os = \\\"vita\\\", target_os = \\\"emscripten\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"netbsd\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"solaris\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"vxworks\\\")\"},{\"default_features\":false,\"name\":\"r-efi\",\"req\":\"^5.1\",\"target\":\"cfg(all(target_os = \\\"uefi\\\", getrandom_backend = \\\"efi_rng\\\"))\"},{\"default_features\":false,\"name\":\"wasip2\",\"req\":\"^1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p2\\\"))\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"}],\"features\":{\"std\":[],\"wasm_js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"]}}", - "getrandom_0.4.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), not(any(all(target_os = \\\"linux\\\", target_env = \\\"\\\"), getrandom_backend = \\\"custom\\\", getrandom_backend = \\\"linux_raw\\\", getrandom_backend = \\\"rdrand\\\", getrandom_backend = \\\"rndr\\\"))))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"hurd\\\", target_os = \\\"illumos\\\", target_os = \\\"cygwin\\\", all(target_os = \\\"horizon\\\", target_arch = \\\"arm\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"haiku\\\", target_os = \\\"redox\\\", target_os = \\\"nto\\\", target_os = \\\"aix\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\", target_os = \\\"tvos\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"openbsd\\\", target_os = \\\"vita\\\", target_os = \\\"emscripten\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"netbsd\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"solaris\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"vxworks\\\")\"},{\"default_features\":false,\"name\":\"r-efi\",\"req\":\"^5.1\",\"target\":\"cfg(all(target_os = \\\"uefi\\\", getrandom_backend = \\\"efi_rng\\\"))\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"wasip2\",\"req\":\"^1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p2\\\"))\"},{\"name\":\"wasip3\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p3\\\"))\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"}],\"features\":{\"std\":[],\"sys_rng\":[\"dep:rand_core\"],\"wasm_js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"]}}", "gif_0.14.1": "{\"dependencies\":[{\"name\":\"color_quant\",\"optional\":true,\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"png\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.10.0\"},{\"name\":\"weezl\",\"req\":\"^0.1.10\"}],\"features\":{\"color_quant\":[\"dep:color_quant\"],\"default\":[\"raii_no_panic\",\"std\",\"color_quant\"],\"raii_no_panic\":[],\"std\":[]}}", "gimli_0.32.3": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"test-assembler\",\"req\":\"^0.1.3\"}],\"features\":{\"default\":[\"read-all\",\"write\"],\"endian-reader\":[\"read\",\"dep:stable_deref_trait\"],\"fallible-iterator\":[\"dep:fallible-iterator\"],\"read\":[\"read-core\"],\"read-all\":[\"read\",\"std\",\"fallible-iterator\",\"endian-reader\"],\"read-core\":[],\"rustc-dep-of-std\":[\"dep:core\",\"dep:alloc\"],\"std\":[\"fallible-iterator?/std\",\"stable_deref_trait?/std\"],\"write\":[\"dep:indexmap\"]}}", "git+https://github.com/dzbarsky/rules_rust?rev=b56cbaa8465e74127f1ea216f813cd377295ad81#b56cbaa8465e74127f1ea216f813cd377295ad81_runfiles": "{\"dependencies\":[],\"features\":{},\"strip_prefix\":\"\"}", @@ -861,7 +902,7 @@ "hyper-rustls_0.27.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"hyper\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"server-auto\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"tls12\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"native-tokio\",\"http1\",\"tls12\",\"logging\",\"aws-lc-rs\"],\"fips\":[\"aws-lc-rs\",\"rustls/fips\"],\"http1\":[\"hyper-util/http1\"],\"http2\":[\"hyper-util/http2\"],\"logging\":[\"log\",\"tokio-rustls/logging\",\"rustls/logging\"],\"native-tokio\":[\"rustls-native-certs\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"tokio-rustls/tls12\",\"rustls/tls12\"],\"webpki-tokio\":[\"webpki-roots\"]}}", "hyper-timeout_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"hyper\",\"req\":\"^1.1\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"hyper-tls\",\"req\":\"^0.6\"},{\"features\":[\"client-legacy\",\"http1\"],\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"features\":[\"client-legacy\",\"http1\",\"server\",\"server-graceful\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.35\"},{\"features\":[\"io-std\",\"io-util\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.35\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{}}", "hyper-tls_0.6.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"name\":\"native-tls\",\"req\":\"^0.2.1\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io-std\",\"macros\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"name\":\"tokio-native-tls\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{\"alpn\":[\"native-tls/alpn\"],\"vendored\":[\"native-tls/vendored\"]}}", - "hyper-util_0.1.20": "{\"dependencies\":[{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.16\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.16\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1.8.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.4.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.9\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pnet_datalink\",\"req\":\"^0.35.0\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"macos\\\"))\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\">=0.5.9, <0.7\"},{\"name\":\"system-configuration\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"test-util\",\"signal\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tower-layer\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"windows-registry\",\"optional\":true,\"req\":\">=0.3, <0.7\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_happy_eyeballs_tests\":[],\"client\":[\"hyper/client\",\"tokio/net\",\"dep:tracing\",\"dep:futures-channel\",\"dep:tower-service\"],\"client-legacy\":[\"client\",\"dep:socket2\",\"tokio/sync\",\"dep:libc\",\"dep:futures-util\"],\"client-pool\":[\"client\",\"dep:futures-util\",\"dep:tower-layer\"],\"client-proxy\":[\"client\",\"dep:base64\",\"dep:ipnet\",\"dep:percent-encoding\"],\"client-proxy-system\":[\"dep:system-configuration\",\"dep:windows-registry\"],\"default\":[],\"full\":[\"client\",\"client-legacy\",\"client-pool\",\"client-proxy\",\"client-proxy-system\",\"server\",\"server-auto\",\"server-graceful\",\"service\",\"http1\",\"http2\",\"tokio\",\"tracing\"],\"http1\":[\"hyper/http1\"],\"http2\":[\"hyper/http2\"],\"server\":[\"hyper/server\"],\"server-auto\":[\"server\",\"http1\",\"http2\"],\"server-graceful\":[\"server\",\"tokio/sync\"],\"service\":[\"dep:tower-service\"],\"tokio\":[\"dep:tokio\",\"tokio/rt\",\"tokio/time\"],\"tracing\":[\"dep:tracing\"]}}", + "hyper-util_0.1.19": "{\"dependencies\":[{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.16\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.16\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1.8.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.4.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.9\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pnet_datalink\",\"req\":\"^0.35.0\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"macos\\\"))\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\">=0.5.9, <0.7\"},{\"name\":\"system-configuration\",\"optional\":true,\"req\":\">=0.5, <0.7\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"test-util\",\"signal\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tower-layer\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"windows-registry\",\"optional\":true,\"req\":\">=0.3, <0.7\",\"target\":\"cfg(windows)\"}],\"features\":{\"__internal_happy_eyeballs_tests\":[],\"client\":[\"hyper/client\",\"tokio/net\",\"dep:tracing\",\"dep:futures-channel\",\"dep:tower-service\"],\"client-legacy\":[\"client\",\"dep:socket2\",\"tokio/sync\",\"dep:libc\",\"dep:futures-util\"],\"client-pool\":[\"client\",\"dep:futures-util\",\"dep:tower-layer\"],\"client-proxy\":[\"client\",\"dep:base64\",\"dep:ipnet\",\"dep:percent-encoding\"],\"client-proxy-system\":[\"dep:system-configuration\",\"dep:windows-registry\"],\"default\":[],\"full\":[\"client\",\"client-legacy\",\"client-pool\",\"client-proxy\",\"client-proxy-system\",\"server\",\"server-auto\",\"server-graceful\",\"service\",\"http1\",\"http2\",\"tokio\",\"tracing\"],\"http1\":[\"hyper/http1\"],\"http2\":[\"hyper/http2\"],\"server\":[\"hyper/server\"],\"server-auto\":[\"server\",\"http1\",\"http2\"],\"server-graceful\":[\"server\",\"tokio/sync\"],\"service\":[\"dep:tower-service\"],\"tokio\":[\"dep:tokio\",\"tokio/rt\",\"tokio/time\"],\"tracing\":[\"dep:tracing\"]}}", "hyper_1.8.1": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"optional\":true,\"req\":\"^1.1.2\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"form_urlencoded\",\"req\":\"^1\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"sink\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4.2\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"httparse\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"itoa\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2.4\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.4\"},{\"name\":\"pin-utils\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.12\"},{\"kind\":\"dev\",\"name\":\"spmc\",\"req\":\"^0.3\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"fs\",\"macros\",\"net\",\"io-std\",\"io-util\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7.10\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"want\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"capi\":[],\"client\":[\"dep:want\",\"dep:pin-project-lite\",\"dep:smallvec\"],\"default\":[],\"ffi\":[\"dep:http-body-util\",\"dep:futures-util\"],\"full\":[\"client\",\"http1\",\"http2\",\"server\"],\"http1\":[\"dep:atomic-waker\",\"dep:futures-channel\",\"dep:futures-core\",\"dep:httparse\",\"dep:itoa\",\"dep:pin-utils\"],\"http2\":[\"dep:futures-channel\",\"dep:futures-core\",\"dep:h2\"],\"nightly\":[],\"server\":[\"dep:httpdate\",\"dep:pin-project-lite\",\"dep:smallvec\"],\"tracing\":[\"dep:tracing\"]}}", "i18n-config_0.4.8": "{\"dependencies\":[{\"name\":\"basic-toml\",\"req\":\"^0.1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"serde\"],\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", "i18n-embed-fl_0.9.4": "{\"dependencies\":[{\"name\":\"dashmap\",\"optional\":true,\"req\":\"^6.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"find-crate\",\"req\":\"^0.6\"},{\"name\":\"fluent\",\"req\":\"^0.16\"},{\"name\":\"fluent-syntax\",\"req\":\"^0.11\"},{\"name\":\"i18n-config\",\"req\":\"^0.4.7\"},{\"features\":[\"fluent-system\",\"filesystem-assets\"],\"name\":\"i18n-embed\",\"req\":\"^0.15.4\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"name\":\"proc-macro-error2\",\"req\":\"^2.0.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rust-embed\",\"req\":\"^8.0\"},{\"name\":\"strsim\",\"req\":\"^0.11\"},{\"features\":[\"derive\",\"proc-macro\",\"parsing\",\"printing\",\"extra-traits\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{\"dashmap\":[\"dep:dashmap\"]}}", @@ -880,7 +921,6 @@ "icu_properties_2.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"icu_collections\",\"req\":\"~2.1.1\"},{\"default_features\":false,\"features\":[\"zerovec\"],\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"icu_properties_data\",\"optional\":true,\"req\":\"~2.1.2\"},{\"default_features\":false,\"name\":\"icu_provider\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"name\":\"unicode-bidi\",\"optional\":true,\"req\":\"^0.3.11\"},{\"default_features\":false,\"features\":[\"yoke\",\"zerofrom\"],\"name\":\"zerotrie\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\",\"yoke\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"zerovec/alloc\",\"icu_collections/alloc\",\"serde?/alloc\"],\"compiled_data\":[\"dep:icu_properties_data\",\"icu_provider/baked\"],\"datagen\":[\"serde\",\"dep:databake\",\"zerovec/databake\",\"icu_collections/databake\",\"icu_locale_core/databake\",\"zerotrie/databake\",\"icu_provider/export\"],\"default\":[\"compiled_data\"],\"serde\":[\"dep:serde\",\"icu_locale_core/serde\",\"zerovec/serde\",\"icu_collections/serde\",\"icu_provider/serde\",\"zerotrie/serde\"],\"unicode_bidi\":[\"dep:unicode-bidi\"]}}", "icu_properties_data_2.1.2": "{\"dependencies\":[],\"features\":{}}", "icu_provider_2.1.1": "{\"dependencies\":[{\"name\":\"bincode\",\"optional\":true,\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"name\":\"erased-serde\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"default_features\":false,\"name\":\"postcard\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.45\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.2.0\"},{\"default_features\":false,\"name\":\"writeable\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerotrie\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"icu_locale_core/alloc\",\"serde?/alloc\",\"yoke/alloc\",\"zerofrom/alloc\",\"zerovec/alloc\",\"zerotrie?/alloc\",\"dep:stable_deref_trait\",\"dep:writeable\"],\"baked\":[\"dep:zerotrie\",\"dep:writeable\"],\"deserialize_bincode_1\":[\"serde\",\"dep:bincode\",\"std\"],\"deserialize_json\":[\"serde\",\"dep:serde_json\"],\"deserialize_postcard_1\":[\"serde\",\"dep:postcard\"],\"export\":[\"serde\",\"dep:erased-serde\",\"dep:databake\",\"std\",\"sync\",\"dep:postcard\",\"zerovec/databake\"],\"logging\":[\"dep:log\"],\"serde\":[\"dep:serde\",\"yoke/serde\"],\"std\":[\"alloc\"],\"sync\":[],\"zerotrie\":[]}}", - "id-arena_2.3.0": "{\"dependencies\":[{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "ident_case_1.0.1": "{\"dependencies\":[],\"features\":{}}", "idna_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"idna_adapter\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"const_generics\"],\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"tester\",\"req\":\"^0.9\"},{\"name\":\"utf8_iter\",\"req\":\"^1.0.4\"}],\"features\":{\"alloc\":[],\"compiled_data\":[\"idna_adapter/compiled_data\"],\"default\":[\"std\",\"compiled_data\"],\"std\":[\"alloc\"]}}", "idna_adapter_1.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"icu_normalizer\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"icu_properties\",\"req\":\"^2\"}],\"features\":{\"compiled_data\":[\"icu_normalizer/compiled_data\",\"icu_properties/compiled_data\"]}}", @@ -901,7 +941,7 @@ "instability_0.3.11": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"indoc\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"name\":\"quote\",\"req\":\"^1.0.25\"},{\"features\":[\"derive\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.15\"}],\"features\":{}}", "intl-memoizer_0.5.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"fluent-langneg\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"intl_pluralrules\",\"req\":\"^7.0\"},{\"name\":\"type-map\",\"req\":\"^0.5\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", "intl_pluralrules_7.0.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", - "inventory_0.3.22": "{\"dependencies\":[{\"name\":\"rustversion\",\"req\":\"^1.0\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{}}", + "inventory_0.3.21": "{\"dependencies\":[{\"name\":\"rustversion\",\"req\":\"^1.0\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\"}],\"features\":{}}", "io_tee_0.1.1": "{\"dependencies\":[],\"features\":{}}", "ipconfig_0.3.2": "{\"dependencies\":[{\"name\":\"socket2\",\"req\":\"^0.5.1\",\"target\":\"cfg(windows)\"},{\"name\":\"widestring\",\"req\":\"^1.0.2\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_Registry\"],\"name\":\"windows-sys\",\"req\":\"^0.48.0\",\"target\":\"cfg(windows)\"},{\"name\":\"winreg\",\"optional\":true,\"req\":\"^0.50.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"computer\":[\"winreg\"],\"default\":[\"computer\"]}}", "ipnet_2.11.0": "{\"dependencies\":[{\"name\":\"heapless\",\"optional\":true,\"req\":\"^0\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"package\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"json\":[\"serde\",\"schemars\"],\"ser_as_str\":[\"heapless\"],\"std\":[]}}", @@ -913,12 +953,12 @@ "itertools_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itertools_0.14.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itoa_1.0.17": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{}}", - "jiff-static_0.2.21": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.93\"},{\"name\":\"quote\",\"req\":\"^1.0.38\"},{\"name\":\"syn\",\"req\":\"^2.0.98\"}],\"features\":{\"default\":[],\"perf-inline\":[],\"tz-fat\":[],\"tzdb\":[\"dep:jiff-tzdb\"]}}", - "jiff_0.2.21": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.81\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"hifitime\",\"req\":\"^3.9.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"humantime\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.39.0\"},{\"name\":\"jiff-static\",\"req\":\"=0.2.21\",\"target\":\"cfg(any())\"},{\"name\":\"jiff-static\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"jiff-tzdb-platform\",\"optional\":true,\"req\":\"^0.1.3\",\"target\":\"cfg(any(windows, target_family = \\\"wasm\\\"))\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.50\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.21\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"req\":\"^1.10.0\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"name\":\"portable-atomic-util\",\"req\":\"^0.2.4\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.117\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.9.34\"},{\"kind\":\"dev\",\"name\":\"tabwriter\",\"req\":\"^1.4.0\"},{\"features\":[\"local-offset\",\"macros\",\"parsing\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.36\"},{\"kind\":\"dev\",\"name\":\"time-tz\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"tzfile\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.70\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_System_Time\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\">=0.52.0, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"portable-atomic-util/alloc\"],\"default\":[\"std\",\"tz-system\",\"tz-fat\",\"tzdb-bundle-platform\",\"tzdb-zoneinfo\",\"tzdb-concatenated\",\"perf-inline\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"logging\":[\"dep:log\"],\"perf-inline\":[],\"serde\":[\"dep:serde_core\"],\"static\":[\"static-tz\",\"jiff-static?/tzdb\"],\"static-tz\":[\"dep:jiff-static\"],\"std\":[\"alloc\",\"log?/std\",\"serde_core?/std\"],\"tz-fat\":[\"jiff-static?/tz-fat\"],\"tz-system\":[\"std\",\"dep:windows-sys\"],\"tzdb-bundle-always\":[\"dep:jiff-tzdb\",\"alloc\"],\"tzdb-bundle-platform\":[\"dep:jiff-tzdb-platform\",\"alloc\"],\"tzdb-concatenated\":[\"std\"],\"tzdb-zoneinfo\":[\"std\"]}}", + "jiff-static_0.2.18": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.93\"},{\"name\":\"quote\",\"req\":\"^1.0.38\"},{\"name\":\"syn\",\"req\":\"^2.0.98\"}],\"features\":{\"default\":[],\"perf-inline\":[],\"tz-fat\":[],\"tzdb\":[\"dep:jiff-tzdb\"]}}", + "jiff_0.2.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.81\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"hifitime\",\"req\":\"^3.9.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"humantime\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.39.0\"},{\"name\":\"jiff-static\",\"req\":\"=0.2.18\",\"target\":\"cfg(any())\"},{\"name\":\"jiff-static\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"jiff-tzdb-platform\",\"optional\":true,\"req\":\"^0.1.3\",\"target\":\"cfg(any(windows, target_family = \\\"wasm\\\"))\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.50\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.21\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"req\":\"^1.10.0\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"name\":\"portable-atomic-util\",\"req\":\"^0.2.4\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.117\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.9.34\"},{\"kind\":\"dev\",\"name\":\"tabwriter\",\"req\":\"^1.4.0\"},{\"features\":[\"local-offset\",\"macros\",\"parsing\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.36\"},{\"kind\":\"dev\",\"name\":\"tzfile\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.70\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_System_Time\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\">=0.52.0, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"portable-atomic-util/alloc\"],\"default\":[\"std\",\"tz-system\",\"tz-fat\",\"tzdb-bundle-platform\",\"tzdb-zoneinfo\",\"tzdb-concatenated\",\"perf-inline\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"logging\":[\"dep:log\"],\"perf-inline\":[],\"serde\":[\"dep:serde_core\"],\"static\":[\"static-tz\",\"jiff-static?/tzdb\"],\"static-tz\":[\"dep:jiff-static\"],\"std\":[\"alloc\",\"log?/std\",\"serde_core?/std\"],\"tz-fat\":[\"jiff-static?/tz-fat\"],\"tz-system\":[\"std\",\"dep:windows-sys\"],\"tzdb-bundle-always\":[\"dep:jiff-tzdb\",\"alloc\"],\"tzdb-bundle-platform\":[\"dep:jiff-tzdb-platform\",\"alloc\"],\"tzdb-concatenated\":[\"std\"],\"tzdb-zoneinfo\":[\"std\"]}}", "jni-sys_0.3.0": "{\"dependencies\":[],\"features\":{}}", - "jni_0.21.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.0\",\"target\":\"cfg(windows)\"},{\"name\":\"cesu8\",\"req\":\"^1.1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"combine\",\"req\":\"^4.1.0\"},{\"name\":\"java-locator\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0.20\"},{\"kind\":\"build\",\"name\":\"walkdir\",\"req\":\"^2\"},{\"features\":[\"Win32_Globalization\"],\"name\":\"windows-sys\",\"req\":\"^0.45.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"invocation\":[\"java-locator\",\"libloading\"]}}", + "jni_0.21.1": "{\"dependencies\":[{\"name\":\"cesu8\",\"req\":\"^1.1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"combine\",\"req\":\"^4.1.0\"},{\"name\":\"java-locator\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"thiserror\",\"req\":\"^1.0.20\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"walkdir\",\"req\":\"^2\"},{\"features\":[\"Win32_Globalization\"],\"name\":\"windows-sys\",\"req\":\"^0.45.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"invocation\":[\"java-locator\",\"libloading\"]}}", "jobserver_0.1.34": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"getrandom\",\"req\":\"^0.3.2\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2.171\",\"target\":\"cfg(unix)\"},{\"features\":[\"fs\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.28.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"}],\"features\":{}}", - "js-sys_0.3.88": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.111\"}],\"features\":{\"default\":[\"std\",\"unsafe-eval\"],\"std\":[\"wasm-bindgen/std\"],\"unsafe-eval\":[]}}", + "js-sys_0.3.85": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"wasm-bindgen/std\"]}}", "keyring_3.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\",\"wrap_help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.1\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.5\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"features\":[\"std\"],\"name\":\"linux-keyutils\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.22\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.66\"},{\"kind\":\"dev\",\"name\":\"rpassword\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"rprompt\",\"req\":\"^2\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^2\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"whoami\",\"req\":\"^1.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Credentials\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.60\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"apple-native\":[\"dep:security-framework\"],\"async-io\":[\"zbus?/async-io\"],\"async-secret-service\":[\"dep:secret-service\",\"dep:zbus\"],\"crypto-openssl\":[\"dbus-secret-service?/crypto-openssl\",\"secret-service?/crypto-openssl\"],\"crypto-rust\":[\"dbus-secret-service?/crypto-rust\",\"secret-service?/crypto-rust\"],\"linux-native\":[\"dep:linux-keyutils\"],\"linux-native-async-persistent\":[\"linux-native\",\"async-secret-service\"],\"linux-native-sync-persistent\":[\"linux-native\",\"sync-secret-service\"],\"sync-secret-service\":[\"dep:dbus-secret-service\"],\"tokio\":[\"zbus?/tokio\"],\"vendored\":[\"dbus-secret-service?/vendored\",\"openssl?/vendored\"],\"windows-native\":[\"dep:windows-sys\",\"dep:byteorder\"]}}", "kqueue-sys_1.0.4": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.2.1\"},{\"name\":\"libc\",\"req\":\"^0.2.74\"}],\"features\":{}}", "kqueue_1.1.1": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"dhat\",\"req\":\"^0.3.2\"},{\"name\":\"kqueue-sys\",\"req\":\"^1.0.4\"},{\"name\":\"libc\",\"req\":\"^0.2.17\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"}],\"features\":{}}", @@ -927,7 +967,6 @@ "landlock_0.4.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"enumflags2\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2.175\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.26\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"}],\"features\":{}}", "language-tags_0.3.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}", "lazy_static_1.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"once\"],\"name\":\"spin\",\"optional\":true,\"req\":\"^0.9.8\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{\"spin_no_std\":[\"spin\"]}}", - "leb128fmt_0.1.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[]}}", "libc_0.2.182": "{\"dependencies\":[{\"name\":\"rustc-std-workspace-core\",\"optional\":true,\"req\":\"^1.0.1\"}],\"features\":{\"align\":[],\"const-extern-fn\":[],\"default\":[\"std\"],\"extra_traits\":[],\"rustc-dep-of-std\":[\"align\",\"rustc-std-workspace-core\"],\"std\":[],\"use_std\":[\"std\"]}}", "libdbus-sys_0.2.7": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.0.78\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"default\":[\"pkg-config\"],\"vendored\":[\"cc\"]}}", "libloading_0.8.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -937,7 +976,7 @@ "libz-sys_1.1.23": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}", "linked-hash-map_0.5.6": "{\"dependencies\":[{\"name\":\"heapsize\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"heapsize_impl\":[\"heapsize\"],\"nightly\":[],\"serde_impl\":[\"serde\"]}}", "linux-keyutils_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.11\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.132\"},{\"kind\":\"dev\",\"name\":\"zeroize\",\"req\":\"^1.5.7\"}],\"features\":{\"default\":[],\"std\":[\"bitflags/std\"]}}", - "linux-raw-sys_0.12.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"if_tun\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"vm_sockets\":[],\"xdp\":[]}}", + "linux-raw-sys_0.11.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}", "linux-raw-sys_0.4.15": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"compiler_builtins\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}", "litemap_0.8.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"serde_core\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"}],\"features\":{\"alloc\":[],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\",\"alloc\"],\"testing\":[\"alloc\"],\"yoke\":[\"dep:yoke\"]}}", "local-waker_0.1.4": "{\"dependencies\":[],\"features\":{}}", @@ -959,7 +998,7 @@ "matchit_0.9.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"wayfind\",\"req\":\"^0.8\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "md-5_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"md5-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"md5-asm\"],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", "md5_0.8.0": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", - "memchr_2.8.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", + "memchr_2.7.6": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", "memoffset_0.6.5": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"autocfg\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"}],\"features\":{\"default\":[],\"unstable_const\":[]}}", "memoffset_0.9.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"autocfg\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"}],\"features\":{\"default\":[],\"unstable_const\":[],\"unstable_offset_of\":[]}}", "mime_0.3.17": "{\"dependencies\":[],\"features\":{}}", @@ -970,7 +1009,7 @@ "moka_0.12.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2.8\"},{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8.3\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.19\"},{\"name\":\"async-lock\",\"optional\":true,\"req\":\"^3.3\"},{\"name\":\"crossbeam-channel\",\"req\":\"^0.5.15\"},{\"name\":\"crossbeam-epoch\",\"req\":\"^0.9.18\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.0\"},{\"name\":\"equivalent\",\"req\":\"^1.0\"},{\"name\":\"event-listener\",\"optional\":true,\"req\":\"^5.3\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(moka_loom)\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.7\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"portable-atomic\",\"req\":\"^1.6\"},{\"name\":\"quanta\",\"optional\":true,\"req\":\"^0.12.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"default_features\":false,\"features\":[\"rustls-tls\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"smallvec\",\"req\":\"^1.8\"},{\"name\":\"tagptr\",\"req\":\"^0.2\"},{\"features\":[\"fs\",\"io-util\",\"macros\",\"rt-multi-thread\",\"sync\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\",\"target\":\"cfg(trybuild)\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"req\":\"^1.1\"}],\"features\":{\"atomic64\":[],\"default\":[],\"future\":[\"async-lock\",\"event-listener\",\"futures-util\"],\"logging\":[\"log\"],\"quanta\":[\"dep:quanta\"],\"sync\":[],\"unstable-debug-counters\":[\"future\"]}}", "moxcms_0.7.11": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"pxfm\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"avx\":[],\"avx512\":[],\"default\":[\"avx\",\"sse\",\"neon\"],\"neon\":[],\"options\":[],\"sse\":[]}}", "multimap_0.10.1": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"serde_impl\"],\"serde_impl\":[\"serde\"]}}", - "native-tls_0.2.18": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.170\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.27\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl\",\"req\":\"^0.10.75\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl-probe\",\"req\":\"^0.2.1\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl-sys\",\"req\":\"^0.9.111\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"schannel\",\"req\":\"^0.1.28\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"security-framework\",\"req\":\"^3.5.1\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"security-framework-sys\",\"req\":\"^2.15.0\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"test-cert-gen\",\"req\":\"^0.9\"}],\"features\":{\"alpn\":[\"security-framework/alpn\"],\"alpn-accept\":[],\"default\":[],\"vendored\":[\"openssl/vendored\"]}}", + "native-tls_0.2.14": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.5\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl\",\"req\":\"^0.10.69\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl-probe\",\"req\":\"^0.1\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"openssl-sys\",\"req\":\"^0.9.81\",\"target\":\"cfg(not(any(target_os = \\\"windows\\\", target_vendor = \\\"apple\\\")))\"},{\"name\":\"schannel\",\"req\":\"^0.1.17\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"security-framework\",\"req\":\"^2.0.0\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"security-framework-sys\",\"req\":\"^2.0.0\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"test-cert-gen\",\"req\":\"^0.9\"}],\"features\":{\"alpn\":[\"security-framework/alpn\"],\"vendored\":[\"openssl/vendored\"]}}", "ndk-context_0.1.1": "{\"dependencies\":[],\"features\":{}}", "ndk-sys_0.5.0+25.2.9519653": "{\"dependencies\":[{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"}],\"features\":{\"audio\":[],\"bitmap\":[],\"media\":[],\"sync\":[],\"test\":[]}}", "ndk_0.8.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.0.0\"},{\"name\":\"ffi\",\"package\":\"ndk-sys\",\"req\":\"^0.5.0\"},{\"name\":\"jni\",\"optional\":true,\"req\":\"^0.21\"},{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"num_enum\",\"req\":\"^0.7\"},{\"name\":\"rwh_04\",\"optional\":true,\"package\":\"raw-window-handle\",\"req\":\"^0.4\"},{\"name\":\"rwh_05\",\"optional\":true,\"package\":\"raw-window-handle\",\"req\":\"^0.5\"},{\"name\":\"rwh_06\",\"optional\":true,\"package\":\"raw-window-handle\",\"req\":\"^0.6\"},{\"name\":\"thiserror\",\"req\":\"^1.0.23\"}],\"features\":{\"all\":[\"audio\",\"bitmap\",\"media\",\"api-level-31\",\"rwh_04\",\"rwh_05\",\"rwh_06\"],\"api-level-23\":[],\"api-level-24\":[\"api-level-23\"],\"api-level-25\":[\"api-level-24\"],\"api-level-26\":[\"api-level-25\"],\"api-level-27\":[\"api-level-26\"],\"api-level-28\":[\"api-level-27\"],\"api-level-29\":[\"api-level-28\"],\"api-level-30\":[\"api-level-29\"],\"api-level-31\":[\"api-level-30\"],\"audio\":[\"ffi/audio\",\"api-level-26\"],\"bitmap\":[\"ffi/bitmap\"],\"default\":[\"rwh_06\"],\"media\":[\"ffi/media\"],\"sync\":[\"ffi/sync\",\"api-level-26\"],\"test\":[\"ffi/test\",\"jni\",\"all\"]}}", @@ -1025,6 +1064,7 @@ "onig_sys_69.9.1": "{\"dependencies\":[{\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.71\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.16\"}],\"features\":{\"default\":[\"generate\"],\"generate\":[\"bindgen\"],\"posix-api\":[],\"print-debug\":[]}}", "opaque-debug_0.3.1": "{\"dependencies\":[],\"features\":{}}", "openssl-macros_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", + "openssl-probe_0.1.6": "{\"dependencies\":[],\"features\":{}}", "openssl-probe_0.2.1": "{\"dependencies\":[],\"features\":{}}", "openssl-src_300.5.5+3.5.5": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.0.79\"}],\"features\":{\"camellia\":[],\"default\":[],\"force-engine\":[],\"idea\":[],\"ktls\":[],\"legacy\":[],\"no-dso\":[],\"seed\":[],\"ssl3\":[],\"weak-crypto\":[]}}", "openssl-sys_0.9.111": "{\"dependencies\":[{\"features\":[\"ssl\",\"bindgen\"],\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13\"},{\"features\":[\"ssl\"],\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.27\"},{\"features\":[\"experimental\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"name\":\"bssl-sys\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.61\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"features\":[\"legacy\"],\"kind\":\"build\",\"name\":\"openssl-src\",\"optional\":true,\"req\":\"^300.2.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.8\"}],\"features\":{\"aws-lc\":[\"dep:aws-lc-sys\"],\"aws-lc-fips\":[\"dep:aws-lc-fips-sys\"],\"unstable_boringssl\":[\"bssl-sys\"],\"vendored\":[\"openssl-src\"]}}", @@ -1037,7 +1077,7 @@ "opentelemetry_0.31.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"js-sys\",\"req\":\"^0.3.63\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"os_rng\",\"thread_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"thiserror\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\">=0.1.40\"}],\"features\":{\"default\":[\"trace\",\"metrics\",\"logs\",\"internal-logs\",\"futures\"],\"futures\":[\"futures-core\",\"futures-sink\",\"pin-project-lite\"],\"internal-logs\":[\"tracing\"],\"logs\":[],\"metrics\":[],\"spec_unstable_logs_enabled\":[\"logs\"],\"testing\":[\"trace\"],\"trace\":[\"futures\",\"thiserror\"]}}", "opentelemetry_sdk_0.31.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"name\":\"futures-executor\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\",\"sink\",\"async-await-macro\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"http\",\"optional\":true,\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"opentelemetry\",\"req\":\"^0.31\"},{\"default_features\":false,\"name\":\"opentelemetry-http\",\"optional\":true,\"req\":\"^0.31\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.0\"},{\"features\":[\"flamegraph\",\"criterion\"],\"kind\":\"dev\",\"name\":\"pprof\",\"req\":\"^0.14\",\"target\":\"cfg(not(target_os = \\\"windows\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\",\"small_rng\",\"os_rng\",\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.23.0\"},{\"default_features\":false,\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"temp-env\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"url\",\"optional\":true,\"req\":\"^2.5\"}],\"features\":{\"default\":[\"trace\",\"metrics\",\"logs\",\"internal-logs\"],\"experimental_async_runtime\":[],\"experimental_logs_batch_log_processor_with_async_runtime\":[\"logs\",\"experimental_async_runtime\"],\"experimental_logs_concurrent_log_processor\":[\"logs\"],\"experimental_metrics_custom_reader\":[\"metrics\"],\"experimental_metrics_disable_name_validation\":[\"metrics\"],\"experimental_metrics_periodicreader_with_async_runtime\":[\"metrics\",\"experimental_async_runtime\"],\"experimental_trace_batch_span_processor_with_async_runtime\":[\"tokio/sync\",\"trace\",\"experimental_async_runtime\"],\"internal-logs\":[\"opentelemetry/internal-logs\"],\"jaeger_remote_sampler\":[\"trace\",\"opentelemetry-http\",\"http\",\"serde\",\"serde_json\",\"url\",\"experimental_async_runtime\"],\"logs\":[\"opentelemetry/logs\"],\"metrics\":[\"opentelemetry/metrics\"],\"rt-tokio\":[\"tokio/rt\",\"tokio/time\",\"tokio-stream\",\"experimental_async_runtime\"],\"rt-tokio-current-thread\":[\"tokio/rt\",\"tokio/time\",\"tokio-stream\",\"experimental_async_runtime\"],\"spec_unstable_logs_enabled\":[\"logs\",\"opentelemetry/spec_unstable_logs_enabled\"],\"spec_unstable_metrics_views\":[\"metrics\"],\"testing\":[\"opentelemetry/testing\",\"trace\",\"metrics\",\"logs\",\"rt-tokio\",\"rt-tokio-current-thread\",\"tokio/macros\",\"tokio/rt-multi-thread\"],\"trace\":[\"opentelemetry/trace\",\"rand\",\"percent-encoding\"]}}", "option-ext_0.2.0": "{\"dependencies\":[],\"features\":{}}", - "ordered-stream_0.2.0": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.25\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.25\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"}],\"features\":{}}", + "ordered-stream_0.2.0": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.25\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.25\"}],\"features\":{}}", "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\"]}}", @@ -1065,7 +1105,7 @@ "pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}", "pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}", "plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}", - "png_0.18.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}", + "png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}", "polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}", "portable-atomic-util_0.2.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"req\":\"^1.5.1\"}],\"features\":{\"alloc\":[],\"default\":[],\"std\":[\"alloc\"]}}", @@ -1075,21 +1115,20 @@ "powerfmt_0.2.0": "{\"dependencies\":[{\"name\":\"powerfmt-macros\",\"optional\":true,\"req\":\"=0.1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"macros\"],\"macros\":[\"dep:powerfmt-macros\"],\"std\":[\"alloc\"]}}", "ppv-lite86_0.2.21": "{\"dependencies\":[{\"features\":[\"simd\"],\"name\":\"zerocopy\",\"req\":\"^0.8.23\"}],\"features\":{\"default\":[\"std\"],\"no_simd\":[],\"simd\":[],\"std\":[]}}", "precomputed-hash_0.1.1": "{\"dependencies\":[],\"features\":{}}", - "predicates-core_1.0.10": "{\"dependencies\":[],\"features\":{}}", - "predicates-tree_1.0.13": "{\"dependencies\":[{\"features\":[\"color\"],\"kind\":\"dev\",\"name\":\"predicates\",\"req\":\"^3.1\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"name\":\"termtree\",\"req\":\"^0.5.0\"}],\"features\":{}}", - "predicates_3.1.4": "{\"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\":[]}}", + "predicates-core_1.0.9": "{\"dependencies\":[],\"features\":{}}", + "predicates-tree_1.0.12": "{\"dependencies\":[{\"features\":[\"color\"],\"kind\":\"dev\",\"name\":\"predicates\",\"req\":\"^3.1\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"name\":\"termtree\",\"req\":\"^0.5.0\"}],\"features\":{}}", + "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\"]}}", "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\"]}}", "proc-macro2_1.0.106": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4\"},{\"name\":\"unicode-ident\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"proc-macro\"],\"nightly\":[],\"proc-macro\":[],\"span-locations\":[]}}", "process-wrap_9.0.1": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.30\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"default_features\":false,\"features\":[\"fs\",\"poll\",\"signal\"],\"name\":\"nix\",\"optional\":true,\"req\":\"^0.30.1\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"remoteprocess\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.20.0\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38.2\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\",\"rt-multi-thread\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"name\":\"windows\",\"optional\":true,\"req\":\"^0.62.2\",\"target\":\"cfg(windows)\"}],\"features\":{\"creation-flags\":[\"dep:windows\",\"windows/Win32_System_Threading\"],\"default\":[\"creation-flags\",\"job-object\",\"kill-on-drop\",\"process-group\",\"process-session\",\"tracing\"],\"job-object\":[\"dep:windows\",\"windows/Win32_Security\",\"windows/Win32_System_Diagnostics_ToolHelp\",\"windows/Win32_System_IO\",\"windows/Win32_System_JobObjects\",\"windows/Win32_System_Threading\"],\"kill-on-drop\":[],\"process-group\":[],\"process-session\":[\"process-group\"],\"reset-sigmask\":[],\"std\":[\"dep:nix\"],\"tokio1\":[\"dep:nix\",\"dep:futures\",\"dep:tokio\"],\"tracing\":[\"dep:tracing\"]}}", - "proptest_1.10.0": "{\"dependencies\":[{\"name\":\"bit-set\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bitflags\",\"req\":\"^2.10\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.15\"},{\"name\":\"proptest-macro\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"req\":\"^0.9\"},{\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rusty-fork\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.115\"},{\"name\":\"unarray\",\"req\":\"^0.1.4\"},{\"name\":\"x86\",\"optional\":true,\"req\":\"^0.52.0\"}],\"features\":{\"alloc\":[],\"atomic64bit\":[],\"attr-macro\":[\"proptest-macro\"],\"bit-set\":[\"dep:bit-set\",\"dep:bit-vec\"],\"default\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"default-code-coverage\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"fork\":[\"std\",\"rusty-fork\",\"tempfile\"],\"handle-panics\":[\"std\"],\"hardware-rng\":[\"x86\"],\"no_std\":[\"num-traits/libm\"],\"std\":[\"rand/std\",\"rand/os_rng\",\"regex-syntax\",\"num-traits/std\"],\"timeout\":[\"fork\",\"rusty-fork/timeout\"],\"unstable\":[]}}", + "proptest_1.9.0": "{\"dependencies\":[{\"name\":\"bit-set\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bitflags\",\"req\":\"^2.9\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.15\"},{\"name\":\"proptest-macro\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"req\":\"^0.9\"},{\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rusty-fork\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.112\"},{\"name\":\"unarray\",\"req\":\"^0.1.4\"},{\"name\":\"x86\",\"optional\":true,\"req\":\"^0.52.0\"}],\"features\":{\"alloc\":[],\"atomic64bit\":[],\"attr-macro\":[\"proptest-macro\"],\"bit-set\":[\"dep:bit-set\",\"dep:bit-vec\"],\"default\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"default-code-coverage\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"fork\":[\"std\",\"rusty-fork\",\"tempfile\"],\"handle-panics\":[\"std\"],\"hardware-rng\":[\"x86\"],\"no_std\":[\"num-traits/libm\"],\"std\":[\"rand/std\",\"rand/os_rng\",\"regex-syntax\",\"num-traits/std\"],\"timeout\":[\"fork\",\"rusty-fork/timeout\"],\"unstable\":[]}}", "prost-derive_0.14.3": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.14\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "prost_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"std\":[]}}", "psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}", - "psl_2.1.193": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}", + "psl_2.1.184": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}", "pulldown-cmark-escape_0.10.1": "{\"dependencies\":[],\"features\":{\"simd\":[]}}", "pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}", "pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}", @@ -1130,15 +1169,15 @@ "rayon_1.11.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"either\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"name\":\"rayon-core\",\"req\":\"^1.13.0\"},{\"name\":\"wasm_sync\",\"optional\":true,\"req\":\"^0.1.0\"}],\"features\":{\"web_spin_lock\":[\"dep:wasm_sync\",\"rayon-core/web_spin_lock\"]}}", "rcgen_0.14.7": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.13.3\"},{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\",\"target\":\"cfg(unix)\"},{\"name\":\"pem\",\"optional\":true,\"req\":\"^3.0.2\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.4.1\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"name\":\"time\",\"req\":\"^0.3.6\"},{\"name\":\"x509-parser\",\"optional\":true,\"req\":\"^0.18\"},{\"features\":[\"time\",\"std\"],\"name\":\"yasna\",\"req\":\"^0.5.2\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.2\"}],\"features\":{\"aws_lc_rs\":[\"crypto\",\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"x509-parser?/verify-aws\"],\"aws_lc_rs_unstable\":[\"aws_lc_rs\",\"aws-lc-rs/unstable\",\"x509-parser?/verify-aws\"],\"crypto\":[],\"default\":[\"crypto\",\"pem\",\"ring\"],\"fips\":[\"crypto\",\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"ring\":[\"crypto\",\"dep:ring\",\"x509-parser?/verify\"]}}", "redox_syscall_0.5.18": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{\"default\":[\"userspace\"],\"rustc-dep-of-std\":[\"core\",\"bitflags/rustc-dep-of-std\"],\"std\":[],\"userspace\":[]}}", - "redox_syscall_0.7.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{\"default\":[\"userspace\"],\"rustc-dep-of-std\":[\"core\",\"bitflags/rustc-dep-of-std\"],\"std\":[],\"userspace\":[]}}", + "redox_syscall_0.7.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{\"default\":[\"userspace\"],\"rustc-dep-of-std\":[\"core\",\"bitflags/rustc-dep-of-std\"],\"std\":[],\"userspace\":[]}}", "redox_users_0.4.6": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\",\"call\"],\"name\":\"libredox\",\"req\":\"^0.1.3\"},{\"name\":\"rust-argon2\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"auth\":[\"rust-argon2\",\"zeroize\"],\"default\":[\"auth\"]}}", "redox_users_0.5.2": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\",\"call\"],\"name\":\"libredox\",\"req\":\"^0.1.3\"},{\"name\":\"rust-argon2\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"auth\":[\"rust-argon2\",\"zeroize\"],\"default\":[\"auth\"]}}", "ref-cast-impl_1.0.25": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1\"},{\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{}}", "ref-cast_1.0.25": "{\"dependencies\":[{\"name\":\"ref-cast-impl\",\"req\":\"=1.0.25\"},{\"kind\":\"dev\",\"name\":\"ref-cast-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{}}", - "regex-automata_0.4.14": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"bstr\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.14\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"syntax\",\"perf\",\"unicode\",\"meta\",\"nfa\",\"dfa\",\"hybrid\"],\"dfa\":[\"dfa-build\",\"dfa-search\",\"dfa-onepass\"],\"dfa-build\":[\"nfa-thompson\",\"dfa-search\"],\"dfa-onepass\":[\"nfa-thompson\"],\"dfa-search\":[],\"hybrid\":[\"alloc\",\"nfa-thompson\"],\"internal-instrument\":[\"internal-instrument-pikevm\"],\"internal-instrument-pikevm\":[\"logging\",\"std\"],\"logging\":[\"dep:log\",\"aho-corasick?/logging\",\"memchr?/logging\"],\"meta\":[\"syntax\",\"nfa-pikevm\"],\"nfa\":[\"nfa-thompson\",\"nfa-pikevm\",\"nfa-backtrack\"],\"nfa-backtrack\":[\"nfa-thompson\"],\"nfa-pikevm\":[\"nfa-thompson\"],\"nfa-thompson\":[\"alloc\"],\"perf\":[\"perf-inline\",\"perf-literal\"],\"perf-inline\":[],\"perf-literal\":[\"perf-literal-substring\",\"perf-literal-multisubstring\"],\"perf-literal-multisubstring\":[\"dep:aho-corasick\"],\"perf-literal-substring\":[\"aho-corasick?/perf-literal\",\"dep:memchr\"],\"std\":[\"regex-syntax?/std\",\"memchr?/std\",\"aho-corasick?/std\",\"alloc\"],\"syntax\":[\"dep:regex-syntax\",\"alloc\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"unicode-word-boundary\",\"regex-syntax?/unicode\"],\"unicode-age\":[\"regex-syntax?/unicode-age\"],\"unicode-bool\":[\"regex-syntax?/unicode-bool\"],\"unicode-case\":[\"regex-syntax?/unicode-case\"],\"unicode-gencat\":[\"regex-syntax?/unicode-gencat\"],\"unicode-perl\":[\"regex-syntax?/unicode-perl\"],\"unicode-script\":[\"regex-syntax?/unicode-script\"],\"unicode-segment\":[\"regex-syntax?/unicode-segment\"],\"unicode-word-boundary\":[]}}", - "regex-lite_0.1.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"string\"],\"std\":[],\"string\":[]}}", + "regex-automata_0.4.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"bstr\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.14\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"syntax\",\"perf\",\"unicode\",\"meta\",\"nfa\",\"dfa\",\"hybrid\"],\"dfa\":[\"dfa-build\",\"dfa-search\",\"dfa-onepass\"],\"dfa-build\":[\"nfa-thompson\",\"dfa-search\"],\"dfa-onepass\":[\"nfa-thompson\"],\"dfa-search\":[],\"hybrid\":[\"alloc\",\"nfa-thompson\"],\"internal-instrument\":[\"internal-instrument-pikevm\"],\"internal-instrument-pikevm\":[\"logging\",\"std\"],\"logging\":[\"dep:log\",\"aho-corasick?/logging\",\"memchr?/logging\"],\"meta\":[\"syntax\",\"nfa-pikevm\"],\"nfa\":[\"nfa-thompson\",\"nfa-pikevm\",\"nfa-backtrack\"],\"nfa-backtrack\":[\"nfa-thompson\"],\"nfa-pikevm\":[\"nfa-thompson\"],\"nfa-thompson\":[\"alloc\"],\"perf\":[\"perf-inline\",\"perf-literal\"],\"perf-inline\":[],\"perf-literal\":[\"perf-literal-substring\",\"perf-literal-multisubstring\"],\"perf-literal-multisubstring\":[\"dep:aho-corasick\"],\"perf-literal-substring\":[\"aho-corasick?/perf-literal\",\"dep:memchr\"],\"std\":[\"regex-syntax?/std\",\"memchr?/std\",\"aho-corasick?/std\",\"alloc\"],\"syntax\":[\"dep:regex-syntax\",\"alloc\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"unicode-word-boundary\",\"regex-syntax?/unicode\"],\"unicode-age\":[\"regex-syntax?/unicode-age\"],\"unicode-bool\":[\"regex-syntax?/unicode-bool\"],\"unicode-case\":[\"regex-syntax?/unicode-case\"],\"unicode-gencat\":[\"regex-syntax?/unicode-gencat\"],\"unicode-perl\":[\"regex-syntax?/unicode-perl\"],\"unicode-script\":[\"regex-syntax?/unicode-script\"],\"unicode-segment\":[\"regex-syntax?/unicode-segment\"],\"unicode-word-boundary\":[]}}", + "regex-lite_0.1.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"string\"],\"std\":[],\"string\":[]}}", "regex-syntax_0.6.29": "{\"dependencies\":[],\"features\":{\"default\":[\"unicode\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", - "regex-syntax_0.8.9": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\",\"unicode\"],\"std\":[],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", + "regex-syntax_0.8.8": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\",\"unicode\"],\"std\":[],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", "regex_1.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"syntax\",\"meta\",\"nfa-pikevm\"],\"name\":\"regex-automata\",\"req\":\"^0.4.12\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"perf\",\"unicode\",\"regex-syntax/default\"],\"logging\":[\"aho-corasick?/logging\",\"memchr?/logging\",\"regex-automata/logging\"],\"pattern\":[],\"perf\":[\"perf-cache\",\"perf-dfa\",\"perf-onepass\",\"perf-backtrack\",\"perf-inline\",\"perf-literal\"],\"perf-backtrack\":[\"regex-automata/nfa-backtrack\"],\"perf-cache\":[],\"perf-dfa\":[\"regex-automata/hybrid\"],\"perf-dfa-full\":[\"regex-automata/dfa-build\",\"regex-automata/dfa-search\"],\"perf-inline\":[\"regex-automata/perf-inline\"],\"perf-literal\":[\"dep:aho-corasick\",\"dep:memchr\",\"regex-automata/perf-literal\"],\"perf-onepass\":[\"regex-automata/dfa-onepass\"],\"std\":[\"aho-corasick?/std\",\"memchr?/std\",\"regex-automata/std\",\"regex-syntax/std\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"regex-automata/unicode\",\"regex-syntax/unicode\"],\"unicode-age\":[\"regex-automata/unicode-age\",\"regex-syntax/unicode-age\"],\"unicode-bool\":[\"regex-automata/unicode-bool\",\"regex-syntax/unicode-bool\"],\"unicode-case\":[\"regex-automata/unicode-case\",\"regex-syntax/unicode-case\"],\"unicode-gencat\":[\"regex-automata/unicode-gencat\",\"regex-syntax/unicode-gencat\"],\"unicode-perl\":[\"regex-automata/unicode-perl\",\"regex-automata/unicode-word-boundary\",\"regex-syntax/unicode-perl\"],\"unicode-script\":[\"regex-automata/unicode-script\",\"regex-syntax/unicode-script\"],\"unicode-segment\":[\"regex-automata/unicode-segment\",\"regex-syntax/unicode-segment\"],\"unstable\":[\"pattern\"],\"use_std\":[\"std\"]}}", "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\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", @@ -1156,14 +1195,14 @@ "rustc_version_0.4.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"semver\",\"req\":\"^1.0\"}],\"features\":{}}", "rusticata-macros_4.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"nom\",\"req\":\"^7.0\"}],\"features\":{}}", "rustix_0.38.44": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.161\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.5.2\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"))\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_NetworkManagement_IpHelper\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"procfs\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"cc\":[],\"default\":[\"std\",\"use-libc-auxv\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"linux-raw-sys/io_uring\"],\"libc-extra-traits\":[\"libc?/extra_traits\"],\"linux_4_11\":[],\"linux_latest\":[\"linux_4_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[\"fs\"],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"procfs\":[\"once_cell\",\"itoa\",\"fs\"],\"pty\":[\"itoa\",\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"compiler_builtins\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\",\"compiler_builtins?/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\",\"libc-extra-traits\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\",\"libc-extra-traits\"],\"use-libc-auxv\":[]}}", - "rustix_1.1.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", + "rustix_1.1.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", "rustls-webpki_0.103.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", "rustversion_1.0.22": "{\"dependencies\":[{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", "rustyline_14.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.2\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"default_features\":false,\"name\":\"buffer-redux\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"clipboard-win\",\"req\":\"^5.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"fd-lock\",\"optional\":true,\"req\":\"^4.0.0\"},{\"name\":\"home\",\"optional\":true,\"req\":\"^0.5.4\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"memchr\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"fs\",\"ioctl\",\"poll\",\"signal\",\"term\"],\"name\":\"nix\",\"req\":\"^0.28\",\"target\":\"cfg(unix)\"},{\"name\":\"radix_trie\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"bundled\",\"backup\"],\"name\":\"rusqlite\",\"optional\":true,\"req\":\"^0.31.0\"},{\"name\":\"rustyline-derive\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"signal-hook\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"skim\",\"optional\":true,\"req\":\"^0.10\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"name\":\"termios\",\"optional\":true,\"req\":\"^0.3.3\",\"target\":\"cfg(unix)\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.0\"},{\"name\":\"unicode-width\",\"req\":\"^0.1\"},{\"name\":\"utf8parse\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Security\",\"Win32_System_Threading\",\"Win32_UI_Input_KeyboardAndMouse\"],\"name\":\"windows-sys\",\"req\":\"^0.52.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"case_insensitive_history_search\":[\"regex\"],\"custom-bindings\":[\"radix_trie\"],\"default\":[\"custom-bindings\",\"with-dirs\",\"with-file-history\"],\"derive\":[\"rustyline-derive\"],\"with-dirs\":[\"home\"],\"with-file-history\":[\"fd-lock\"],\"with-fuzzy\":[\"skim\"],\"with-sqlite-history\":[\"rusqlite\"]}}", - "ryu_1.0.23": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.5\"}],\"features\":{\"small\":[]}}", + "ryu_1.0.22": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"}],\"features\":{\"small\":[]}}", "salsa20_0.10.2": "{\"dependencies\":[{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"std\":[\"cipher/std\"],\"zeroize\":[\"cipher/zeroize\"]}}", "same-file_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"winapi-util\",\"req\":\"^0.1.1\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "scc_2.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"sdd\",\"req\":\"^3.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.47\"}],\"features\":{\"loom\":[\"dep:loom\",\"sdd/loom\"]}}", @@ -1183,21 +1222,21 @@ "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\"]}}", - "security-framework-sys_2.17.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.7\"},{\"name\":\"libc\",\"req\":\"^0.2.150\"}],\"features\":{\"OSX_10_10\":[],\"OSX_10_11\":[],\"OSX_10_12\":[],\"OSX_10_13\":[],\"OSX_10_14\":[],\"OSX_10_15\":[],\"OSX_10_9\":[],\"default\":[\"OSX_10_13\"],\"macos-12\":[]}}", + "security-framework-sys_2.15.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2.150\"}],\"features\":{\"OSX_10_10\":[\"OSX_10_9\"],\"OSX_10_11\":[\"OSX_10_10\"],\"OSX_10_12\":[\"OSX_10_11\"],\"OSX_10_13\":[\"OSX_10_12\"],\"OSX_10_14\":[\"OSX_10_13\"],\"OSX_10_15\":[\"OSX_10_14\"],\"OSX_10_9\":[],\"default\":[\"OSX_10_12\"]}}", "security-framework_2.11.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.6\"},{\"name\":\"core-foundation\",\"req\":\"^0.9.4\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"libc\",\"req\":\"^0.2.139\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.6\"},{\"default_features\":false,\"name\":\"security-framework-sys\",\"req\":\"^2.11.1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.17\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.16\"}],\"features\":{\"OSX_10_10\":[\"OSX_10_9\",\"security-framework-sys/OSX_10_10\"],\"OSX_10_11\":[\"OSX_10_10\",\"security-framework-sys/OSX_10_11\"],\"OSX_10_12\":[\"OSX_10_11\",\"security-framework-sys/OSX_10_12\"],\"OSX_10_13\":[\"OSX_10_12\",\"security-framework-sys/OSX_10_13\",\"alpn\",\"session-tickets\",\"serial-number-bigint\"],\"OSX_10_14\":[\"OSX_10_13\",\"security-framework-sys/OSX_10_14\"],\"OSX_10_15\":[\"OSX_10_14\",\"security-framework-sys/OSX_10_15\"],\"OSX_10_9\":[\"security-framework-sys/OSX_10_9\"],\"alpn\":[],\"default\":[\"OSX_10_12\"],\"job-bless\":[],\"nightly\":[],\"serial-number-bigint\":[\"dep:num-bigint\"],\"session-tickets\":[]}}", - "security-framework_3.7.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.11\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"libc\",\"req\":\"^0.2.139\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"name\":\"security-framework-sys\",\"req\":\"^2.17\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.12.0\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.23\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{\"OSX_10_12\":[],\"OSX_10_13\":[],\"OSX_10_14\":[],\"OSX_10_15\":[\"security-framework-sys/OSX_10_15\"],\"alpn\":[],\"default\":[\"OSX_10_14\",\"alpn\",\"session-tickets\"],\"job-bless\":[],\"macos-12\":[\"security-framework-sys/macos-12\"],\"nightly\":[],\"session-tickets\":[],\"sync-keychain\":[\"OSX_10_13\"]}}", + "security-framework_3.5.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.6\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"libc\",\"req\":\"^0.2.139\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"name\":\"security-framework-sys\",\"req\":\"^2.15\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.12.0\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.23\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.16\"}],\"features\":{\"OSX_10_12\":[\"security-framework-sys/OSX_10_12\"],\"OSX_10_13\":[\"OSX_10_12\",\"security-framework-sys/OSX_10_13\",\"alpn\",\"session-tickets\"],\"OSX_10_14\":[\"OSX_10_13\",\"security-framework-sys/OSX_10_14\"],\"OSX_10_15\":[\"OSX_10_14\",\"security-framework-sys/OSX_10_15\"],\"alpn\":[],\"default\":[\"OSX_10_12\"],\"job-bless\":[],\"nightly\":[],\"session-tickets\":[],\"sync-keychain\":[\"OSX_10_13\"]}}", "self_cell_0.10.3": "{\"dependencies\":[{\"name\":\"new_self_cell\",\"package\":\"self_cell\",\"req\":\"^1\"}],\"features\":{\"old_rust\":[\"new_self_cell/old_rust\"]}}", "self_cell_1.2.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"=1.1.0\"},{\"name\":\"rustversion\",\"optional\":true,\"req\":\">=1\"}],\"features\":{\"old_rust\":[\"rustversion\"]}}", "semver_1.0.27": "{\"dependencies\":[{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"package\":\"serde_core\",\"req\":\"^1.0.220\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde\"],\"std\":[]}}", - "sentry-actix_0.46.2": "{\"dependencies\":[{\"name\":\"actix-http\",\"req\":\"^3.10\"},{\"default_features\":false,\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"bytes\",\"req\":\"^1.11.1\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"}],\"features\":{\"default\":[\"release-health\"],\"release-health\":[\"sentry-core/release-health\"]}}", - "sentry-backtrace_0.46.2": "{\"dependencies\":[{\"name\":\"backtrace\",\"req\":\"^0.3.44\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-perl\"],\"name\":\"regex\",\"req\":\"^1.5.5\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.2\"}],\"features\":{}}", - "sentry-contexts_0.46.2": "{\"dependencies\":[{\"name\":\"hostname\",\"req\":\"^0.4\"},{\"name\":\"libc\",\"req\":\"^0.2.66\"},{\"name\":\"os_info\",\"req\":\"^3.5.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.2\"},{\"name\":\"uname\",\"req\":\"^0.1.1\",\"target\":\"cfg(not(windows))\"}],\"features\":{}}", - "sentry-core_0.46.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.30\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.24\"},{\"features\":[\"std\"],\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.5.3\"},{\"name\":\"sentry-types\",\"req\":\"^0.46.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.104\"},{\"name\":\"serde_json\",\"req\":\"^1.0.46\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"name\":\"url\",\"req\":\"^2.1.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.0.0\"}],\"features\":{\"client\":[\"rand\"],\"default\":[],\"logs\":[],\"release-health\":[],\"test\":[\"client\",\"release-health\"]}}", - "sentry-debug-images_0.46.2": "{\"dependencies\":[{\"name\":\"findshlibs\",\"req\":\"=0.10.2\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.2\"}],\"features\":{}}", - "sentry-panic_0.46.2": "{\"dependencies\":[{\"name\":\"sentry-backtrace\",\"req\":\"^0.46.2\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.2\"}],\"features\":{}}", - "sentry-tracing_0.46.2": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.9.4\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"sentry-backtrace\",\"optional\":true,\"req\":\"^0.46.2\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.2\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.20\"},{\"features\":[\"fmt\",\"registry\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.20\"}],\"features\":{\"backtrace\":[\"dep:sentry-backtrace\"],\"default\":[],\"logs\":[\"sentry-core/logs\"]}}", - "sentry-types_0.46.2": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"debugid\",\"req\":\"^0.8.0\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.25.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.104\"},{\"name\":\"serde_json\",\"req\":\"^1.0.46\"},{\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"features\":[\"formatting\",\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.5\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.1.1\"},{\"features\":[\"serde\"],\"name\":\"uuid\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"protocol\"],\"protocol\":[]}}", - "sentry_0.46.2": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.30\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2.1\"},{\"name\":\"curl\",\"optional\":true,\"req\":\"^0.4.25\"},{\"name\":\"embedded-svc\",\"optional\":true,\"req\":\"^0.28.1\"},{\"name\":\"esp-idf-svc\",\"optional\":true,\"req\":\"^0.51.0\",\"target\":\"cfg(target_os = \\\"espidf\\\")\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"blocking\",\"json\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12.25\"},{\"default_features\":false,\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.18\"},{\"default_features\":false,\"name\":\"sentry-actix\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-anyhow\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-backtrace\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-contexts\",\"optional\":true,\"req\":\"^0.46.2\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.2\"},{\"name\":\"sentry-debug-images\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-log\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-opentelemetry\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-panic\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-slog\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-tower\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"sentry-tracing\",\"optional\":true,\"req\":\"^0.46.2\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.48\"},{\"kind\":\"dev\",\"name\":\"slog\",\"req\":\"^2.5.2\"},{\"features\":[\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.44\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"fmt\",\"tracing-log\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"ureq\",\"optional\":true,\"req\":\"^3.0.11\"}],\"features\":{\"actix\":[\"sentry-actix\"],\"anyhow\":[\"sentry-anyhow\"],\"backtrace\":[\"sentry-backtrace\",\"sentry-tracing?/backtrace\"],\"contexts\":[\"sentry-contexts\"],\"curl\":[\"dep:curl\",\"httpdate\"],\"debug-images\":[\"sentry-debug-images\"],\"default\":[\"backtrace\",\"contexts\",\"debug-images\",\"panic\",\"transport\",\"release-health\"],\"embedded-svc-http\":[\"dep:embedded-svc\",\"dep:esp-idf-svc\"],\"log\":[\"sentry-log\"],\"logs\":[\"sentry-core/logs\",\"sentry-tracing?/logs\",\"sentry-log?/logs\"],\"native-tls\":[\"dep:native-tls\",\"reqwest?/default-tls\",\"ureq?/native-tls\"],\"opentelemetry\":[\"sentry-opentelemetry\"],\"panic\":[\"sentry-panic\"],\"release-health\":[\"sentry-core/release-health\",\"sentry-actix?/release-health\"],\"reqwest\":[\"dep:reqwest\",\"httpdate\",\"tokio\"],\"rustls\":[\"dep:rustls\",\"reqwest?/rustls-tls\",\"ureq?/rustls\"],\"slog\":[\"sentry-slog\"],\"test\":[\"sentry-core/test\"],\"tower\":[\"sentry-tower\"],\"tower-axum-matched-path\":[\"tower-http\",\"sentry-tower/axum-matched-path\"],\"tower-http\":[\"tower\",\"sentry-tower/http\"],\"tracing\":[\"sentry-tracing\"],\"transport\":[\"reqwest\",\"native-tls\"],\"ureq\":[\"dep:ureq\",\"httpdate\"]}}", + "sentry-actix_0.46.1": "{\"dependencies\":[{\"name\":\"actix-http\",\"req\":\"^3.10\"},{\"default_features\":false,\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"}],\"features\":{\"default\":[\"release-health\"],\"release-health\":[\"sentry-core/release-health\"]}}", + "sentry-backtrace_0.46.1": "{\"dependencies\":[{\"name\":\"backtrace\",\"req\":\"^0.3.44\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-perl\"],\"name\":\"regex\",\"req\":\"^1.5.5\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.1\"}],\"features\":{}}", + "sentry-contexts_0.46.1": "{\"dependencies\":[{\"name\":\"hostname\",\"req\":\"^0.4\"},{\"name\":\"libc\",\"req\":\"^0.2.66\"},{\"name\":\"os_info\",\"req\":\"^3.5.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.1\"},{\"name\":\"uname\",\"req\":\"^0.1.1\",\"target\":\"cfg(not(windows))\"}],\"features\":{}}", + "sentry-core_0.46.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.30\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.24\"},{\"features\":[\"std\"],\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.5.3\"},{\"name\":\"sentry-types\",\"req\":\"^0.46.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.104\"},{\"name\":\"serde_json\",\"req\":\"^1.0.46\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"name\":\"url\",\"req\":\"^2.1.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.0.0\"}],\"features\":{\"client\":[\"rand\"],\"default\":[],\"logs\":[],\"release-health\":[],\"test\":[\"client\",\"release-health\"]}}", + "sentry-debug-images_0.46.1": "{\"dependencies\":[{\"name\":\"findshlibs\",\"req\":\"=0.10.2\"},{\"name\":\"sentry-core\",\"req\":\"^0.46.1\"}],\"features\":{}}", + "sentry-panic_0.46.1": "{\"dependencies\":[{\"name\":\"sentry-backtrace\",\"req\":\"^0.46.1\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.1\"}],\"features\":{}}", + "sentry-tracing_0.46.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.9.4\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"sentry-backtrace\",\"optional\":true,\"req\":\"^0.46.1\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.20\"},{\"features\":[\"fmt\",\"registry\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.20\"}],\"features\":{\"backtrace\":[\"dep:sentry-backtrace\"],\"default\":[],\"logs\":[\"sentry-core/logs\"]}}", + "sentry-types_0.46.1": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"debugid\",\"req\":\"^0.8.0\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.25.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.104\"},{\"name\":\"serde_json\",\"req\":\"^1.0.46\"},{\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"features\":[\"formatting\",\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.5\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.1.1\"},{\"features\":[\"serde\"],\"name\":\"uuid\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"protocol\"],\"protocol\":[]}}", + "sentry_0.46.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.30\"},{\"name\":\"curl\",\"optional\":true,\"req\":\"^0.4.25\"},{\"name\":\"embedded-svc\",\"optional\":true,\"req\":\"^0.28.1\"},{\"name\":\"esp-idf-svc\",\"optional\":true,\"req\":\"^0.51.0\",\"target\":\"cfg(target_os = \\\"espidf\\\")\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"pretty_env_logger\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"blocking\",\"json\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12.25\"},{\"default_features\":false,\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.18\"},{\"default_features\":false,\"name\":\"sentry-actix\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-anyhow\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-backtrace\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-contexts\",\"optional\":true,\"req\":\"^0.46.1\"},{\"features\":[\"client\"],\"name\":\"sentry-core\",\"req\":\"^0.46.1\"},{\"name\":\"sentry-debug-images\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-log\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-opentelemetry\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-panic\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-slog\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-tower\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"sentry-tracing\",\"optional\":true,\"req\":\"^0.46.1\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.48\"},{\"kind\":\"dev\",\"name\":\"slog\",\"req\":\"^2.5.2\"},{\"features\":[\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.44\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"fmt\",\"tracing-log\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"ureq\",\"optional\":true,\"req\":\"^3.0.11\"}],\"features\":{\"actix\":[\"sentry-actix\"],\"anyhow\":[\"sentry-anyhow\"],\"backtrace\":[\"sentry-backtrace\",\"sentry-tracing?/backtrace\"],\"contexts\":[\"sentry-contexts\"],\"curl\":[\"dep:curl\",\"httpdate\"],\"debug-images\":[\"sentry-debug-images\"],\"default\":[\"backtrace\",\"contexts\",\"debug-images\",\"panic\",\"transport\",\"release-health\"],\"embedded-svc-http\":[\"dep:embedded-svc\",\"dep:esp-idf-svc\"],\"log\":[\"sentry-log\"],\"logs\":[\"sentry-core/logs\",\"sentry-tracing?/logs\",\"sentry-log?/logs\"],\"native-tls\":[\"dep:native-tls\",\"reqwest?/default-tls\",\"ureq?/native-tls\"],\"opentelemetry\":[\"sentry-opentelemetry\"],\"panic\":[\"sentry-panic\"],\"release-health\":[\"sentry-core/release-health\",\"sentry-actix?/release-health\"],\"reqwest\":[\"dep:reqwest\",\"httpdate\",\"tokio\"],\"rustls\":[\"dep:rustls\",\"reqwest?/rustls-tls\",\"ureq?/rustls\"],\"slog\":[\"sentry-slog\"],\"test\":[\"sentry-core/test\"],\"tower\":[\"sentry-tower\"],\"tower-axum-matched-path\":[\"tower-http\",\"sentry-tower/axum-matched-path\"],\"tower-http\":[\"tower\",\"sentry-tower/http\"],\"tracing\":[\"sentry-tracing\"],\"transport\":[\"reqwest\",\"native-tls\"],\"ureq\":[\"dep:ureq\",\"httpdate\"]}}", "serde_1.0.228": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"result\"],\"name\":\"serde_core\",\"req\":\"=1.0.228\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"serde_core/alloc\"],\"default\":[\"std\"],\"derive\":[\"serde_derive\"],\"rc\":[\"serde_core/rc\"],\"std\":[\"serde_core/std\"],\"unstable\":[\"serde_core/unstable\"]}}", "serde_core_1.0.228": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_derive\",\"req\":\"=1.0.228\",\"target\":\"cfg(any())\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"result\"],\"rc\":[],\"result\":[],\"std\":[],\"unstable\":[]}}", "serde_derive_1.0.228": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"derive\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.81\"}],\"features\":{\"default\":[],\"deserialize_in_place\":[]}}", @@ -1211,9 +1250,9 @@ "serde_with_3.16.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"chrono_0_4\",\"optional\":true,\"package\":\"chrono\",\"req\":\"^0.4.20\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"expect-test\",\"req\":\"^1.5.1\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.3\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"hashbrown_0_14\",\"optional\":true,\"package\":\"hashbrown\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"hashbrown_0_15\",\"optional\":true,\"package\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"hashbrown_0_16\",\"optional\":true,\"package\":\"hashbrown\",\"req\":\"^0.16.0\"},{\"default_features\":false,\"name\":\"hex\",\"optional\":true,\"req\":\"^0.4.3\"},{\"default_features\":false,\"features\":[\"serde-1\"],\"name\":\"indexmap_1\",\"optional\":true,\"package\":\"indexmap\",\"req\":\"^1.8\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"indexmap_2\",\"optional\":true,\"package\":\"indexmap\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"resolve-file\"],\"kind\":\"dev\",\"name\":\"jsonschema\",\"req\":\"^0.33.0\"},{\"kind\":\"dev\",\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.12.1\"},{\"kind\":\"dev\",\"name\":\"rmp-serde\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"ron\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.22\"},{\"default_features\":false,\"name\":\"schemars_0_8\",\"optional\":true,\"package\":\"schemars\",\"req\":\"^0.8.16\"},{\"kind\":\"dev\",\"name\":\"schemars_0_8\",\"package\":\"schemars\",\"req\":\"^0.8.16\"},{\"default_features\":false,\"name\":\"schemars_0_9\",\"optional\":true,\"package\":\"schemars\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"schemars_0_9\",\"package\":\"schemars\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"schemars_1\",\"optional\":true,\"package\":\"schemars\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"schemars_1\",\"package\":\"schemars\",\"req\":\"^1.0.2\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.152\"},{\"kind\":\"dev\",\"name\":\"serde-xml-rs\",\"req\":\"^0.8.1\"},{\"default_features\":false,\"features\":[\"result\"],\"name\":\"serde_core\",\"req\":\"^1.0.225\"},{\"default_features\":false,\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.145\"},{\"features\":[\"preserve_order\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.124\"},{\"name\":\"serde_with_macros\",\"optional\":true,\"req\":\"=3.16.1\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"name\":\"smallvec_1\",\"optional\":true,\"package\":\"smallvec\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"time_0_3\",\"optional\":true,\"package\":\"time\",\"req\":\"~0.3.36\"}],\"features\":{\"alloc\":[\"serde_core/alloc\",\"base64?/alloc\",\"chrono_0_4?/alloc\",\"hex?/alloc\",\"serde_json?/alloc\",\"time_0_3?/alloc\"],\"base64\":[\"dep:base64\",\"alloc\"],\"chrono\":[\"chrono_0_4\"],\"chrono_0_4\":[\"dep:chrono_0_4\"],\"default\":[\"std\",\"macros\"],\"guide\":[\"dep:document-features\",\"macros\",\"std\"],\"hashbrown_0_14\":[\"dep:hashbrown_0_14\",\"alloc\"],\"hashbrown_0_15\":[\"dep:hashbrown_0_15\",\"alloc\"],\"hashbrown_0_16\":[\"dep:hashbrown_0_16\",\"alloc\"],\"hex\":[\"dep:hex\",\"alloc\"],\"indexmap\":[\"indexmap_1\"],\"indexmap_1\":[\"dep:indexmap_1\",\"alloc\"],\"indexmap_2\":[\"dep:indexmap_2\",\"alloc\"],\"json\":[\"dep:serde_json\",\"alloc\"],\"macros\":[\"dep:serde_with_macros\"],\"schemars_0_8\":[\"dep:schemars_0_8\",\"std\",\"serde_with_macros?/schemars_0_8\"],\"schemars_0_9\":[\"dep:schemars_0_9\",\"alloc\",\"serde_with_macros?/schemars_0_9\",\"dep:serde_json\"],\"schemars_1\":[\"dep:schemars_1\",\"alloc\",\"serde_with_macros?/schemars_1\",\"dep:serde_json\"],\"smallvec_1\":[\"dep:smallvec_1\"],\"std\":[\"alloc\",\"serde_core/std\",\"chrono_0_4?/clock\",\"chrono_0_4?/std\",\"indexmap_1?/std\",\"indexmap_2?/std\",\"time_0_3?/serde-well-known\",\"time_0_3?/std\",\"schemars_0_9?/std\",\"schemars_1?/std\"],\"time_0_3\":[\"dep:time_0_3\"]}}", "serde_with_macros_3.16.1": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.21.0\"},{\"kind\":\"dev\",\"name\":\"expect-test\",\"req\":\"^1.5.1\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.1\"},{\"name\":\"quote\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.12.1\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.22\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.152\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.25\"},{\"features\":[\"extra-traits\",\"full\",\"parsing\"],\"name\":\"syn\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.111\"}],\"features\":{\"schemars_0_8\":[],\"schemars_0_9\":[],\"schemars_1\":[]}}", "serde_yaml_0.9.34+deprecated": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.79\"},{\"name\":\"indexmap\",\"req\":\"^2.2.1\"},{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2.0\"},{\"name\":\"itoa\",\"req\":\"^1.0\"},{\"name\":\"ryu\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0.195\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.195\"},{\"name\":\"unsafe-libyaml\",\"req\":\"^0.2.11\"}],\"features\":{}}", - "serial2_0.2.34": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert2\",\"req\":\"^0.3.11\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"name\":\"libc\",\"req\":\"^0.2.109\",\"target\":\"cfg(unix)\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.108\"},{\"features\":[\"commapi\",\"fileapi\",\"handleapi\",\"ioapiset\",\"std\",\"synchapi\",\"winbase\",\"winerror\",\"winreg\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{\"doc\":[],\"doc-cfg\":[],\"rs4xx\":[],\"serde\":[\"dep:serde\"],\"unix\":[],\"windows\":[]}}", - "serial_test_3.4.0": "{\"dependencies\":[{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\">=0.6.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"fslock\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"use_std\"],\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\">=0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\">=0.4.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"default_features\":false,\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"scc\",\"req\":\"^2\"},{\"name\":\"serial_test_derive\",\"req\":\"~3.4.0\"}],\"features\":{\"async\":[\"dep:futures-executor\",\"dep:futures-util\",\"serial_test_derive/async\"],\"default\":[\"logging\",\"async\"],\"docsrs\":[\"dep:document-features\"],\"file_locks\":[\"dep:fslock\"],\"logging\":[\"dep:log\"],\"test_logging\":[\"logging\",\"dep:env_logger\",\"serial_test_derive/test_logging\"]}}", - "serial_test_derive_3.4.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\">=0.6.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"printing\",\"parsing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"async\":[],\"default\":[],\"file_locks\":[],\"test_logging\":[]}}", + "serial2_0.2.33": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert2\",\"req\":\"^0.3.11\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"name\":\"libc\",\"req\":\"^0.2.109\",\"target\":\"cfg(unix)\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.108\"},{\"features\":[\"commapi\",\"fileapi\",\"handleapi\",\"ioapiset\",\"std\",\"synchapi\",\"winbase\",\"winerror\",\"winreg\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{\"doc\":[],\"doc-cfg\":[],\"rs4xx\":[],\"serde\":[\"dep:serde\"],\"unix\":[],\"windows\":[]}}", + "serial_test_3.3.1": "{\"dependencies\":[{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\">=0.6.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"fslock\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"use_std\"],\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\">=0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\">=0.4.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"default_features\":false,\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"scc\",\"req\":\"^2\"},{\"name\":\"serial_test_derive\",\"req\":\"~3.3.1\"}],\"features\":{\"async\":[\"dep:futures-executor\",\"dep:futures-util\",\"serial_test_derive/async\"],\"default\":[\"logging\",\"async\"],\"docsrs\":[\"dep:document-features\"],\"file_locks\":[\"dep:fslock\"],\"logging\":[\"dep:log\"],\"test_logging\":[\"logging\",\"dep:env_logger\",\"serial_test_derive/test_logging\"]}}", + "serial_test_derive_3.3.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\">=0.6.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"printing\",\"parsing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"async\":[],\"default\":[],\"file_locks\":[],\"test_logging\":[]}}", "sha1_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha1-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"sha1-asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", "sha1_smol_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "sha2_0.10.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha2-asm\",\"optional\":true,\"req\":\"^0.6.1\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"}],\"features\":{\"asm\":[\"sha2-asm\"],\"asm-aarch64\":[\"asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"force-soft-compact\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", @@ -1265,15 +1304,15 @@ "supports-color_2.1.0": "{\"dependencies\":[{\"name\":\"is-terminal\",\"req\":\"^0.4.0\"},{\"name\":\"is_ci\",\"req\":\"^1.1.1\"}],\"features\":{}}", "supports-color_3.0.2": "{\"dependencies\":[{\"name\":\"is_ci\",\"req\":\"^1.2.0\"}],\"features\":{}}", "syn_1.0.109": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.46\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1.0\"},{\"name\":\"unicode-ident\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.1\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}", - "syn_2.0.117": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.13\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1\"},{\"name\":\"unicode-ident\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\",\"target\":\"cfg(not(miri))\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"dep:quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote?/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}", + "syn_2.0.114": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1\"},{\"features\":[\"blocking\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.13\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"syn-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"tar\",\"req\":\"^0.4.16\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"termcolor\",\"req\":\"^1\"},{\"name\":\"unicode-ident\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\",\"target\":\"cfg(not(miri))\"}],\"features\":{\"clone-impls\":[],\"default\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\"],\"derive\":[],\"extra-traits\":[],\"fold\":[],\"full\":[],\"parsing\":[],\"printing\":[\"dep:quote\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"quote?/proc-macro\"],\"test\":[\"syn-test-suite/all-features\"],\"visit\":[],\"visit-mut\":[]}}", "sync_wrapper_1.0.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"}],\"features\":{\"futures\":[\"futures-core\"]}}", "synstructure_0.13.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"printing\",\"clone-impls\",\"visit\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"synstructure_test_traits\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\",\"syn/proc-macro\",\"quote/proc-macro\"]}}", "syntect_5.3.0": "{\"dependencies\":[{\"name\":\"bincode\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"fancy-regex\",\"optional\":true,\"req\":\"^0.16.2\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"fnv\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"getopts\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"default_features\":false,\"name\":\"onig\",\"optional\":true,\"req\":\"^6.5.1\"},{\"name\":\"plist\",\"optional\":true,\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"public-api\",\"req\":\"^0.50.1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustdoc-json\",\"req\":\"^0.9.7\"},{\"kind\":\"dev\",\"name\":\"rustup-toolchain\",\"req\":\"^0.1.5\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0.12\"},{\"name\":\"walkdir\",\"req\":\"^2.0\"},{\"name\":\"yaml-rust\",\"optional\":true,\"req\":\"^0.4.5\"}],\"features\":{\"default\":[\"default-onig\"],\"default-fancy\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-fancy\"],\"default-onig\":[\"parsing\",\"default-syntaxes\",\"default-themes\",\"html\",\"plist-load\",\"yaml-load\",\"dump-load\",\"dump-create\",\"regex-onig\"],\"default-syntaxes\":[\"parsing\",\"dump-load\"],\"default-themes\":[\"dump-load\"],\"dump-create\":[\"flate2\",\"bincode\"],\"dump-load\":[\"flate2\",\"bincode\"],\"html\":[\"parsing\"],\"metadata\":[\"parsing\",\"plist-load\",\"dep:serde_json\"],\"parsing\":[\"regex-syntax\",\"fnv\",\"dump-create\",\"dump-load\"],\"plist-load\":[\"plist\",\"dep:serde_json\"],\"regex-fancy\":[\"fancy-regex\"],\"regex-onig\":[\"onig\"],\"yaml-load\":[\"yaml-rust\",\"parsing\"]}}", "sys-locale_0.3.2": "{\"dependencies\":[{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"features\":[\"Window\",\"WorkerGlobalScope\",\"Navigator\",\"WorkerNavigator\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"}],\"features\":{\"js\":[\"js-sys\",\"wasm-bindgen\",\"web-sys\"]}}", "system-configuration-sys_0.6.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.149\"}],\"features\":{}}", - "system-configuration_0.7.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}", + "system-configuration_0.6.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}", "tagptr_0.2.0": "{\"dependencies\":[],\"features\":{}}", - "tempfile_3.25.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\">=0.3.0, <0.5\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.3\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}", + "tempfile_3.24.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.3\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}", "term_0.7.0": "{\"dependencies\":[{\"name\":\"dirs-next\",\"req\":\"^2\"},{\"name\":\"rustversion\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"features\":[\"consoleapi\",\"wincon\",\"handleapi\",\"fileapi\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[]}}", "termcolor_1.4.1": "{\"dependencies\":[{\"name\":\"winapi-util\",\"req\":\"^0.1.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "terminal_size_0.4.3": "{\"dependencies\":[{\"features\":[\"termios\"],\"name\":\"rustix\",\"req\":\"^1.0.1\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\"],\"name\":\"windows-sys\",\"req\":\"^0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -1301,21 +1340,21 @@ "tinyvec_macros_0.1.1": "{\"dependencies\":[],\"features\":{}}", "tokio-graceful_0.2.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\",\"target\":\"cfg(not(loom))\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"server\",\"http1\",\"http2\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"server\",\"server-auto\",\"http1\",\"http2\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"futures\",\"checkpoint\"],\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"slab\",\"req\":\"^0.4\"},{\"features\":[\"rt\",\"signal\",\"sync\",\"macros\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"net\",\"rt-multi-thread\",\"io-util\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{}}", "tokio-macros_2.6.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"}],\"features\":{}}", - "tokio-native-tls_0.3.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^0.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.6\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"native-tls\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\",\"target\":\"cfg(all(not(target_os = \\\"macos\\\"), not(windows), not(target_os = \\\"ios\\\")))\"},{\"kind\":\"dev\",\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"security-framework\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"io-util\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.0\"},{\"features\":[\"lmcons\",\"basetsd\",\"minwinbase\",\"minwindef\",\"ntdef\",\"sysinfoapi\",\"timezoneapi\",\"wincrypt\",\"winerror\"],\"kind\":\"dev\",\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"vendored\":[\"native-tls/vendored\"]}}", + "tokio-native-tls_0.3.1": "{\"dependencies\":[{\"name\":\"native-tls\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^0.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.6\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"io-util\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\",\"target\":\"cfg(all(not(target_os = \\\"macos\\\"), not(windows), not(target_os = \\\"ios\\\")))\"},{\"kind\":\"dev\",\"name\":\"security-framework\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"kind\":\"dev\",\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"features\":[\"lmcons\",\"basetsd\",\"minwinbase\",\"minwindef\",\"ntdef\",\"sysinfoapi\",\"timezoneapi\",\"wincrypt\",\"winerror\"],\"kind\":\"dev\",\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"vendored\":[\"native-tls/vendored\"]}}", "tokio-rustls_0.26.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"argh\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.1\"},{\"features\":[\"pem\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"rustls/aws_lc_rs\"],\"brotli\":[\"rustls/brotli\"],\"default\":[\"logging\",\"tls12\",\"aws_lc_rs\"],\"early-data\":[],\"fips\":[\"rustls/fips\"],\"logging\":[\"rustls/logging\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"rustls/tls12\"],\"zlib\":[\"rustls/zlib\"]}}", "tokio-stream_0.1.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"}],\"features\":{\"default\":[\"time\"],\"fs\":[\"tokio/fs\"],\"full\":[\"time\",\"net\",\"io-util\",\"fs\",\"sync\",\"signal\"],\"io-util\":[\"tokio/io-util\"],\"net\":[\"tokio/net\"],\"signal\":[\"tokio/signal\"],\"sync\":[\"tokio/sync\",\"tokio-util\"],\"time\":[\"tokio/time\"]}}", "tokio-test_0.4.5": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.0\"},{\"features\":[\"rt\",\"sync\",\"time\",\"test-util\"],\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"name\":\"tokio-stream\",\"req\":\"^0.1.1\"}],\"features\":{}}", "tokio-util_0.7.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3.0\"},{\"name\":\"bytes\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"futures-sink\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.5\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.44.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\"}],\"features\":{\"__docs_rs\":[\"futures-util\"],\"codec\":[],\"compat\":[\"futures-io\"],\"default\":[],\"full\":[\"codec\",\"compat\",\"io-util\",\"time\",\"net\",\"rt\",\"join-map\"],\"io\":[],\"io-util\":[\"io\",\"tokio/rt\",\"tokio/io-util\"],\"join-map\":[\"rt\",\"hashbrown\"],\"net\":[\"tokio/net\"],\"rt\":[\"tokio/rt\",\"tokio/sync\",\"futures-util\"],\"time\":[\"tokio/time\",\"slab\"]}}", "tokio_1.49.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.2.1\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-concurrency\",\"req\":\"^7.6.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"io-uring\",\"optional\":true,\"req\":\"^0.7.6\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"os-poll\",\"os-ext\"],\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"mio-aio\",\"req\":\"^1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.13.0\"},{\"default_features\":false,\"features\":[\"aio\",\"fs\",\"socket\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\",\"target\":\"cfg(unix)\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"signal-hook-registry\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(unix)\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.9\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"tokio-macros\",\"optional\":true,\"req\":\"~2.6.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\",\"target\":\"cfg(tokio_unstable)\"},{\"kind\":\"dev\",\"name\":\"tracing-mock\",\"req\":\"=0.1.0-beta.1\",\"target\":\"cfg(all(tokio_unstable, target_has_atomic = \\\"64\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.0\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.61\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Authorization\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"fs\":[],\"full\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"process\",\"rt\",\"rt-multi-thread\",\"signal\",\"sync\",\"time\"],\"io-std\":[],\"io-uring\":[\"dep:io-uring\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"dep:slab\"],\"io-util\":[\"bytes\"],\"macros\":[\"tokio-macros\"],\"net\":[\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"socket2\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_Security\",\"windows-sys/Win32_Storage_FileSystem\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_System_SystemServices\"],\"process\":[\"bytes\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Threading\",\"windows-sys/Win32_System_WindowsProgramming\"],\"rt\":[],\"rt-multi-thread\":[\"rt\"],\"signal\":[\"libc\",\"mio/os-poll\",\"mio/net\",\"mio/os-ext\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Console\"],\"sync\":[],\"taskdump\":[\"dep:backtrace\"],\"test-util\":[\"rt\",\"sync\",\"time\"],\"time\":[]}}", "toml_0.5.11": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0.97\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[],\"preserve_order\":[\"indexmap\"]}}", - "toml_0.9.12+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_spanned\",\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_datetime\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.7\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"std\",\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\",\"serde\",\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"fast_hash\":[\"preserve_order\",\"dep:foldhash\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"preserve_order\":[\"dep:indexmap\",\"std\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"serde_spanned/serde\"],\"std\":[\"indexmap?/std\",\"serde_core?/std\",\"toml_parser?/std\",\"toml_writer?/std\",\"toml_datetime/std\",\"serde_spanned/std\"],\"unbounded\":[]}}", + "toml_0.9.11+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_spanned\",\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_datetime\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"std\",\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\",\"serde\",\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"fast_hash\":[\"preserve_order\",\"dep:foldhash\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"preserve_order\":[\"dep:indexmap\",\"std\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"serde_spanned/serde\"],\"std\":[\"indexmap?/std\",\"serde_core?/std\",\"toml_parser?/std\",\"toml_writer?/std\",\"toml_datetime/std\",\"serde_spanned/std\"],\"unbounded\":[]}}", "toml_datetime_0.7.5+spec-1.1.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\"],\"default\":[\"std\"],\"serde\":[\"dep:serde_core\"],\"std\":[\"alloc\",\"serde_core?/std\"]}}", "toml_edit_0.23.10+spec-1.0.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"features\":[\"serde\"],\"name\":\"serde_spanned\",\"optional\":true,\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"name\":\"toml_datetime\",\"req\":\"^0.7.4\"},{\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.5\"},{\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\",\"display\"],\"default\":[\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"dep:serde_spanned\"],\"unbounded\":[]}}", - "toml_edit_0.24.1+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"features\":[\"serde\"],\"name\":\"serde_spanned\",\"optional\":true,\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"name\":\"toml_datetime\",\"req\":\"^0.7.5\"},{\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.7\"},{\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\",\"display\"],\"default\":[\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"dep:serde_spanned\"],\"unbounded\":[]}}", - "toml_parser_1.0.9+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", + "toml_edit_0.24.0+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"features\":[\"serde\"],\"name\":\"serde_spanned\",\"optional\":true,\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"name\":\"toml_datetime\",\"req\":\"^0.7.5\"},{\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.6\"},{\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\",\"display\"],\"default\":[\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"dep:serde_spanned\"],\"unbounded\":[]}}", + "toml_parser_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", "toml_writer_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml_old\",\"package\":\"toml\",\"req\":\"^0.5.11\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", - "tonic-prost_0.14.5": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"prost\",\"req\":\"^0.14\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"tonic\",\"req\":\"^0.14.0\"}],\"features\":{}}", - "tonic_0.14.5": "{\"dependencies\":[{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"hyper-timeout\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.11\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.0\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.1\"},{\"default_features\":false,\"name\":\"tokio-stream\",\"req\":\"^0.1.16\"},{\"default_features\":false,\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"load-shed\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13.0\"}],\"features\":{\"_tls-any\":[\"dep:tokio\",\"tokio?/rt\",\"tokio?/macros\",\"tls-connect-info\"],\"channel\":[\"dep:hyper\",\"hyper?/client\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"dep:tower\",\"tower?/balance\",\"tower?/buffer\",\"tower?/discover\",\"tower?/limit\",\"tower?/load-shed\",\"tower?/util\",\"dep:tokio\",\"tokio?/time\",\"dep:hyper-timeout\"],\"codegen\":[\"dep:async-trait\"],\"default\":[\"router\",\"transport\",\"codegen\"],\"deflate\":[\"dep:flate2\"],\"gzip\":[\"dep:flate2\"],\"router\":[\"dep:axum\",\"dep:tower\",\"tower?/util\"],\"server\":[\"dep:h2\",\"dep:hyper\",\"hyper?/server\",\"dep:hyper-util\",\"hyper-util?/service\",\"hyper-util?/server-auto\",\"dep:socket2\",\"dep:tokio\",\"tokio?/macros\",\"tokio?/net\",\"tokio?/time\",\"tokio-stream/net\",\"dep:tower\",\"tower?/util\",\"tower?/limit\",\"tower?/load-shed\"],\"tls-aws-lc\":[\"_tls-any\",\"tokio-rustls/aws-lc-rs\"],\"tls-connect-info\":[\"dep:tokio-rustls\"],\"tls-native-roots\":[\"_tls-any\",\"channel\",\"dep:rustls-native-certs\"],\"tls-ring\":[\"_tls-any\",\"tokio-rustls/ring\"],\"tls-webpki-roots\":[\"_tls-any\",\"channel\",\"dep:webpki-roots\"],\"transport\":[\"server\",\"channel\"],\"zstd\":[\"dep:zstd\"]}}", + "tonic-prost_0.14.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"prost\",\"req\":\"^0.14\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"tonic\",\"req\":\"^0.14.0\"}],\"features\":{}}", + "tonic_0.14.3": "{\"dependencies\":[{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"hyper-timeout\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.11\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.0\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.1\"},{\"default_features\":false,\"name\":\"tokio-stream\",\"req\":\"^0.1.16\"},{\"default_features\":false,\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"load-shed\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13.0\"}],\"features\":{\"_tls-any\":[\"dep:tokio\",\"tokio?/rt\",\"tokio?/macros\",\"tls-connect-info\"],\"channel\":[\"dep:hyper\",\"hyper?/client\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"dep:tower\",\"tower?/balance\",\"tower?/buffer\",\"tower?/discover\",\"tower?/limit\",\"tower?/load-shed\",\"tower?/util\",\"dep:tokio\",\"tokio?/time\",\"dep:hyper-timeout\"],\"codegen\":[\"dep:async-trait\"],\"default\":[\"router\",\"transport\",\"codegen\"],\"deflate\":[\"dep:flate2\"],\"gzip\":[\"dep:flate2\"],\"router\":[\"dep:axum\",\"dep:tower\",\"tower?/util\"],\"server\":[\"dep:h2\",\"dep:hyper\",\"hyper?/server\",\"dep:hyper-util\",\"hyper-util?/service\",\"hyper-util?/server-auto\",\"dep:socket2\",\"dep:tokio\",\"tokio?/macros\",\"tokio?/net\",\"tokio?/time\",\"tokio-stream/net\",\"dep:tower\",\"tower?/util\",\"tower?/limit\",\"tower?/load-shed\"],\"tls-aws-lc\":[\"_tls-any\",\"tokio-rustls/aws-lc-rs\"],\"tls-connect-info\":[\"dep:tokio-rustls\"],\"tls-native-roots\":[\"_tls-any\",\"channel\",\"dep:rustls-native-certs\"],\"tls-ring\":[\"_tls-any\",\"tokio-rustls/ring\"],\"tls-webpki-roots\":[\"_tls-any\",\"channel\",\"dep:webpki-roots\"],\"transport\":[\"server\",\"channel\"],\"zstd\":[\"dep:zstd\"]}}", "tower-http_0.6.8": "{\"dependencies\":[{\"features\":[\"tokio\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2.0.2\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.14\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.14\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"http-range-header\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"client-legacy\",\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"iri-string\",\"optional\":true,\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.1.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"sync_wrapper\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"buffer\",\"util\",\"retry\",\"make\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"add-extension\":[],\"auth\":[\"base64\",\"validate-request\"],\"catch-panic\":[\"tracing\",\"futures-util/std\",\"dep:http-body\",\"dep:http-body-util\"],\"compression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-full\":[\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"compression-zstd\"],\"compression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"cors\":[],\"decompression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-full\":[\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"decompression-zstd\"],\"decompression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"default\":[],\"follow-redirect\":[\"futures-util\",\"dep:http-body\",\"iri-string\",\"tower/util\"],\"fs\":[\"futures-core\",\"futures-util\",\"dep:http-body\",\"dep:http-body-util\",\"tokio/fs\",\"tokio-util/io\",\"tokio/io-util\",\"dep:http-range-header\",\"mime_guess\",\"mime\",\"percent-encoding\",\"httpdate\",\"set-status\",\"futures-util/alloc\",\"tracing\"],\"full\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-full\",\"cors\",\"decompression-full\",\"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\"],\"limit\":[\"dep:http-body\",\"dep:http-body-util\"],\"map-request-body\":[],\"map-response-body\":[],\"metrics\":[\"dep:http-body\",\"tokio/time\"],\"normalize-path\":[],\"propagate-header\":[],\"redirect\":[],\"request-id\":[\"uuid\"],\"sensitive-headers\":[],\"set-header\":[],\"set-status\":[],\"timeout\":[\"dep:http-body\",\"tokio/time\"],\"trace\":[\"dep:http-body\",\"tracing\"],\"util\":[\"tower\"],\"validate-request\":[\"mime\"]}}", "tower-layer_0.3.3": "{\"dependencies\":[],\"features\":{}}", "tower-service_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.22\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"features\":[\"macros\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"tower-layer\",\"req\":\"^0.3\"}],\"features\":{}}", @@ -1328,8 +1367,8 @@ "tracing-opentelemetry_0.32.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"name\":\"js-sys\",\"req\":\"^0.3.64\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"lazy_static\",\"optional\":true,\"req\":\"^1.0.2\"},{\"default_features\":false,\"features\":[\"trace\"],\"name\":\"opentelemetry\",\"req\":\"^0.31.0\"},{\"features\":[\"trace\",\"metrics\"],\"kind\":\"dev\",\"name\":\"opentelemetry\",\"req\":\"^0.31.0\"},{\"features\":[\"metrics\",\"grpc-tonic\"],\"kind\":\"dev\",\"name\":\"opentelemetry-otlp\",\"req\":\"^0.31.0\"},{\"features\":[\"semconv_experimental\"],\"kind\":\"dev\",\"name\":\"opentelemetry-semantic-conventions\",\"req\":\"^0.31.0\"},{\"features\":[\"trace\",\"metrics\"],\"kind\":\"dev\",\"name\":\"opentelemetry-stdout\",\"req\":\"^0.31.0\"},{\"default_features\":false,\"features\":[\"trace\",\"experimental_metrics_custom_reader\",\"testing\"],\"kind\":\"dev\",\"name\":\"opentelemetry_sdk\",\"req\":\"^0.31.0\"},{\"features\":[\"flamegraph\",\"criterion\"],\"kind\":\"dev\",\"name\":\"pprof\",\"req\":\"^0.15.0\",\"target\":\"cfg(not(target_os = \\\"windows\\\"))\"},{\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"std\",\"attributes\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"},{\"kind\":\"dev\",\"name\":\"tracing-error\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"tracing-log\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"registry\",\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.22\"},{\"default_features\":false,\"features\":[\"registry\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"name\":\"web-time\",\"req\":\"^1.0.0\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\")))\"}],\"features\":{\"default\":[\"tracing-log\",\"metrics\"],\"metrics\":[\"opentelemetry/metrics\",\"smallvec\"]}}", "tracing-serde_0.2.0": "{\"dependencies\":[{\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"},{\"default_features\":false,\"name\":\"valuable-serde\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"},{\"default_features\":false,\"name\":\"valuable_crate\",\"optional\":true,\"package\":\"valuable\",\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"valuable\":[\"valuable_crate\",\"valuable-serde\",\"tracing-core/valuable\"]}}", "tracing-subscriber_0.3.22": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"clock\",\"std\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.26\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"matchers\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"nu-ansi-term\",\"optional\":true,\"req\":\"^0.50.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.13.0\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-automata\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.140\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.82\"},{\"name\":\"sharded-slab\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.9.0\"},{\"name\":\"thread_local\",\"optional\":true,\"req\":\"^1.1.4\"},{\"features\":[\"formatting\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.2\"},{\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.2\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.43\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.43\"},{\"default_features\":false,\"name\":\"tracing-core\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"std-future\",\"std\"],\"kind\":\"dev\",\"name\":\"tracing-futures\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"log-tracer\",\"std\"],\"name\":\"tracing-log\",\"optional\":true,\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"tracing-log\",\"req\":\"^0.2.0\"},{\"name\":\"tracing-serde\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"valuable-serde\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"},{\"default_features\":false,\"name\":\"valuable_crate\",\"optional\":true,\"package\":\"valuable\",\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"alloc\":[],\"ansi\":[\"fmt\",\"nu-ansi-term\"],\"default\":[\"smallvec\",\"fmt\",\"ansi\",\"tracing-log\",\"std\"],\"env-filter\":[\"matchers\",\"once_cell\",\"tracing\",\"std\",\"thread_local\",\"dep:regex-automata\"],\"fmt\":[\"registry\",\"std\"],\"json\":[\"tracing-serde\",\"serde\",\"serde_json\"],\"local-time\":[\"time/local-offset\"],\"nu-ansi-term\":[\"dep:nu-ansi-term\"],\"regex\":[],\"registry\":[\"sharded-slab\",\"thread_local\",\"std\"],\"std\":[\"alloc\",\"tracing-core/std\"],\"valuable\":[\"tracing-core/valuable\",\"valuable_crate\",\"valuable-serde\",\"tracing-serde/valuable\"]}}", - "tracing-test-macro_0.2.6": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"no-env-filter\":[]}}", - "tracing-test_0.2.6": "{\"dependencies\":[{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"tracing-test-macro\",\"req\":\"=0.2.6\"}],\"features\":{\"no-env-filter\":[\"tracing-test-macro/no-env-filter\"]}}", + "tracing-test-macro_0.2.5": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"no-env-filter\":[]}}", + "tracing-test_0.2.5": "{\"dependencies\":[{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-core\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"tracing-test-macro\",\"req\":\"^0.2.5\"}],\"features\":{\"no-env-filter\":[\"tracing-test-macro/no-env-filter\"]}}", "tracing_0.1.44": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.21\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\"},{\"name\":\"tracing-attributes\",\"optional\":true,\"req\":\"^0.1.31\"},{\"default_features\":false,\"name\":\"tracing-core\",\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.38\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"async-await\":[],\"attributes\":[\"tracing-attributes\"],\"default\":[\"std\",\"attributes\"],\"log-always\":[\"log\"],\"max_level_debug\":[],\"max_level_error\":[],\"max_level_info\":[],\"max_level_off\":[],\"max_level_trace\":[],\"max_level_warn\":[],\"release_max_level_debug\":[],\"release_max_level_error\":[],\"release_max_level_info\":[],\"release_max_level_off\":[],\"release_max_level_trace\":[],\"release_max_level_warn\":[],\"std\":[\"tracing-core/std\"],\"valuable\":[\"tracing-core/valuable\"]}}", "tree-sitter-bash_0.25.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"tree-sitter\",\"req\":\"^0.25\"},{\"name\":\"tree-sitter-language\",\"req\":\"^0.1\"}],\"features\":{}}", "tree-sitter-language_0.1.7": "{\"dependencies\":[],\"features\":{}}", @@ -1348,7 +1387,7 @@ "unic-langid_0.9.6": "{\"dependencies\":[{\"name\":\"unic-langid-impl\",\"req\":\"^0.9.6\"},{\"name\":\"unic-langid-macros\",\"optional\":true,\"req\":\"^0.9.6\"},{\"kind\":\"dev\",\"name\":\"unic-langid-macros\",\"req\":\"^0.9.6\"}],\"features\":{\"default\":[],\"likelysubtags\":[\"unic-langid-impl/likelysubtags\"],\"macros\":[\"unic-langid-macros\"],\"serde\":[\"unic-langid-impl/serde\"]}}", "unicase_2.9.0": "{\"dependencies\":[],\"features\":{\"nightly\":[]}}", "unicode-bidi_0.3.18": "{\"dependencies\":[{\"name\":\"flame\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"flamer\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\">=0.8, <2.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\">=0.8, <2.0\"},{\"features\":[\"union\"],\"name\":\"smallvec\",\"optional\":true,\"req\":\">=1.13\"}],\"features\":{\"bench_it\":[],\"default\":[\"std\",\"hardcoded-data\"],\"flame_it\":[\"flame\",\"flamer\"],\"hardcoded-data\":[],\"std\":[],\"unstable\":[],\"with_serde\":[\"serde\"]}}", - "unicode-ident_1.0.24": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"kind\":\"dev\",\"name\":\"fst\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"roaring\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ucd-trie\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"unicode-xid\",\"req\":\"^0.2.6\"}],\"features\":{}}", + "unicode-ident_1.0.22": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"fst\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"roaring\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ucd-trie\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"unicode-xid\",\"req\":\"^0.2.6\"}],\"features\":{}}", "unicode-linebreak_0.1.5": "{\"dependencies\":[],\"features\":{}}", "unicode-normalization_0.1.25": "{\"dependencies\":[{\"features\":[\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "unicode-properties_0.1.4": "{\"dependencies\":[],\"features\":{\"default\":[\"general-category\",\"emoji\"],\"emoji\":[],\"general-category\":[]}}", @@ -1362,13 +1401,13 @@ "untrusted_0.7.1": "{\"dependencies\":[],\"features\":{}}", "untrusted_0.9.0": "{\"dependencies\":[],\"features\":{}}", "ureq-proto_0.5.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"http\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"name\":\"httparse\",\"req\":\"^1.8.0\"},{\"name\":\"log\",\"req\":\"^0.4.22\"}],\"features\":{\"client\":[],\"default\":[\"client\",\"server\"],\"server\":[]}}", - "ureq_3.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_no_alloc\",\"req\":\"^1.1.2\"},{\"kind\":\"dev\",\"name\":\"auto-args\",\"req\":\"^0.3.0\"},{\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"default_features\":false,\"features\":[\"preserve_order\"],\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"pem\",\"std\"],\"name\":\"der\",\"optional\":true,\"req\":\"^0.7.9\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8.34\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.7\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.30\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2.15\"},{\"name\":\"log\",\"req\":\"^0.4.25\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.5\"},{\"default_features\":false,\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.12\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.22\"},{\"features\":[\"aws-lc-rs\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.11.0\"},{\"default_features\":false,\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.138\"},{\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.204\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.120\"},{\"name\":\"socks\",\"optional\":true,\"req\":\"^0.3.4\"},{\"default_features\":false,\"features\":[\"client\"],\"name\":\"ureq-proto\",\"req\":\"^0.5.3\"},{\"default_features\":false,\"name\":\"url\",\"optional\":true,\"req\":\"^2.3.1\"},{\"name\":\"utf-8\",\"req\":\"^0.7.6\"},{\"default_features\":false,\"name\":\"webpki-root-certs\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"winreg\",\"optional\":true,\"req\":\"^0.55\",\"target\":\"cfg(windows)\"}],\"features\":{\"_doc\":[\"rustls?/aws-lc-rs\"],\"_ring\":[\"rustls?/ring\"],\"_rustls\":[],\"_test\":[],\"_tls\":[\"dep:rustls-pki-types\"],\"_url\":[\"dep:url\"],\"brotli\":[\"dep:brotli-decompressor\"],\"charset\":[\"dep:encoding_rs\"],\"cookies\":[\"dep:cookie_store\",\"_url\"],\"default\":[\"rustls\",\"gzip\"],\"gzip\":[\"dep:flate2\"],\"json\":[\"dep:serde\",\"dep:serde_json\",\"cookie_store?/serde_json\"],\"multipart\":[\"dep:mime_guess\",\"dep:getrandom\"],\"native-tls\":[\"native-tls-no-default\",\"dep:der\",\"_tls\",\"native-tls-webpki-roots\"],\"native-tls-no-default\":[\"dep:native-tls\",\"dep:der\",\"_tls\"],\"native-tls-webpki-roots\":[\"dep:webpki-root-certs\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"rustls\":[\"rustls-no-provider\",\"_ring\",\"rustls-webpki-roots\"],\"rustls-no-provider\":[\"dep:rustls\",\"_tls\",\"_rustls\"],\"rustls-webpki-roots\":[\"dep:webpki-roots\"],\"socks-proxy\":[\"dep:socks\"],\"vendored\":[\"native-tls?/vendored\"],\"win-system-proxy\":[\"dep:winreg\"]}}", + "ureq_3.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_no_alloc\",\"req\":\"^1.1.2\"},{\"kind\":\"dev\",\"name\":\"auto-args\",\"req\":\"^0.3.0\"},{\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"default_features\":false,\"features\":[\"preserve_order\"],\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"pem\",\"std\"],\"name\":\"der\",\"optional\":true,\"req\":\"^0.7.9\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8.34\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.7\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.30\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2.15\"},{\"name\":\"log\",\"req\":\"^0.4.25\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.5\"},{\"default_features\":false,\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.12\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.22\"},{\"features\":[\"aws-lc-rs\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.11.0\"},{\"default_features\":false,\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.138\"},{\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.204\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.120\"},{\"name\":\"socks\",\"optional\":true,\"req\":\"^0.3.4\"},{\"default_features\":false,\"features\":[\"client\"],\"name\":\"ureq-proto\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"name\":\"url\",\"optional\":true,\"req\":\"^2.3.1\"},{\"name\":\"utf-8\",\"req\":\"^0.7.6\"},{\"default_features\":false,\"name\":\"webpki-root-certs\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1.0.0\"}],\"features\":{\"_doc\":[\"rustls?/aws-lc-rs\"],\"_ring\":[\"rustls?/ring\"],\"_rustls\":[],\"_test\":[],\"_tls\":[\"dep:rustls-pki-types\"],\"_url\":[\"dep:url\"],\"brotli\":[\"dep:brotli-decompressor\"],\"charset\":[\"dep:encoding_rs\"],\"cookies\":[\"dep:cookie_store\",\"_url\"],\"default\":[\"rustls\",\"gzip\"],\"gzip\":[\"dep:flate2\"],\"json\":[\"dep:serde\",\"dep:serde_json\",\"cookie_store?/serde_json\"],\"multipart\":[\"dep:mime_guess\",\"dep:getrandom\"],\"native-tls\":[\"dep:native-tls\",\"dep:der\",\"_tls\",\"dep:webpki-root-certs\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"rustls\":[\"rustls-no-provider\",\"_ring\"],\"rustls-no-provider\":[\"dep:rustls\",\"_tls\",\"dep:webpki-roots\",\"_rustls\"],\"socks-proxy\":[\"dep:socks\"],\"vendored\":[\"native-tls?/vendored\"]}}", "url_2.5.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"form_urlencoded\",\"req\":\"^1.2.2\"},{\"default_features\":false,\"features\":[\"alloc\",\"compiled_data\"],\"name\":\"idna\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"percent-encoding\",\"req\":\"^2.3.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"debugger_visualizer\":[],\"default\":[\"std\"],\"expose_internals\":[],\"serde\":[\"dep:serde\",\"dep:serde_derive\"],\"std\":[\"idna/std\",\"percent-encoding/std\",\"form_urlencoded/std\",\"serde?/std\"]}}", "urlencoding_2.1.3": "{\"dependencies\":[],\"features\":{}}", "utf-8_0.7.6": "{\"dependencies\":[],\"features\":{}}", "utf8_iter_1.0.4": "{\"dependencies\":[],\"features\":{}}", "utf8parse_0.2.2": "{\"dependencies\":[],\"features\":{\"default\":[],\"nightly\":[]}}", - "uuid_1.21.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"atomic\",\"optional\":true,\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh-derive\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.21.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"md-5\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.56\"},{\"default_features\":false,\"name\":\"sha1_smol\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"slog\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.52\"},{\"name\":\"uuid-rng-internal-lib\",\"optional\":true,\"package\":\"uuid-rng-internal\",\"req\":\"^1.21.0\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"default_features\":false,\"features\":[\"msrv\"],\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"atomic\":[\"dep:atomic\"],\"borsh\":[\"dep:borsh\",\"dep:borsh-derive\"],\"default\":[\"std\"],\"fast-rng\":[\"rng\",\"dep:rand\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"macro-diagnostics\":[],\"md5\":[\"dep:md-5\"],\"rng\":[\"dep:getrandom\"],\"rng-getrandom\":[\"rng\",\"dep:getrandom\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/getrandom\"],\"rng-rand\":[\"rng\",\"dep:rand\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/rand\"],\"serde\":[\"dep:serde_core\"],\"sha1\":[\"dep:sha1_smol\"],\"std\":[\"wasm-bindgen?/std\",\"js-sys?/std\"],\"v1\":[\"atomic\"],\"v3\":[\"md5\"],\"v4\":[\"rng\"],\"v5\":[\"sha1\"],\"v6\":[\"atomic\"],\"v7\":[\"rng\"],\"v8\":[]}}", + "uuid_1.20.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"atomic\",\"optional\":true,\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh-derive\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.20.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"md-5\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.56\"},{\"default_features\":false,\"name\":\"sha1_smol\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"slog\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.52\"},{\"name\":\"uuid-rng-internal-lib\",\"optional\":true,\"package\":\"uuid-rng-internal\",\"req\":\"^1.20.0\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"default_features\":false,\"features\":[\"msrv\"],\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"atomic\":[\"dep:atomic\"],\"borsh\":[\"dep:borsh\",\"dep:borsh-derive\"],\"default\":[\"std\"],\"fast-rng\":[\"rng\",\"dep:rand\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"macro-diagnostics\":[],\"md5\":[\"dep:md-5\"],\"rng\":[\"dep:getrandom\"],\"rng-getrandom\":[\"rng\",\"dep:getrandom\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/getrandom\"],\"rng-rand\":[\"rng\",\"dep:rand\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/rand\"],\"serde\":[\"dep:serde_core\"],\"sha1\":[\"dep:sha1_smol\"],\"std\":[\"wasm-bindgen?/std\",\"js-sys?/std\"],\"v1\":[\"atomic\"],\"v3\":[\"md5\"],\"v4\":[\"rng\"],\"v5\":[\"sha1\"],\"v6\":[\"atomic\"],\"v7\":[\"rng\"],\"v8\":[]}}", "valuable_0.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"valuable-derive\",\"optional\":true,\"req\":\"=0.1.1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"derive\":[\"valuable-derive\"],\"std\":[\"alloc\"]}}", "vcpkg_0.2.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3.7\"}],\"features\":{}}", "version_check_0.9.5": "{\"dependencies\":[],\"features\":{}}", @@ -1379,29 +1418,25 @@ "want_0.3.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"tokio-executor\",\"req\":\"^0.2.0-alpha.2\"},{\"kind\":\"dev\",\"name\":\"tokio-sync\",\"req\":\"^0.2.0-alpha.2\"},{\"name\":\"try-lock\",\"req\":\"^0.2.4\"}],\"features\":{}}", "wasi_0.11.1+wasi-snapshot-preview1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\"],\"std\":[]}}", "wasip2_1.0.2+wasi-0.2.9": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"wit-bindgen\",\"req\":\"^0.51.0\"}],\"features\":{\"bitflags\":[\"wit-bindgen/bitflags\"],\"default\":[\"std\",\"bitflags\"],\"rustc-dep-of-std\":[\"core\",\"alloc\",\"wit-bindgen/rustc-dep-of-std\"],\"std\":[]}}", - "wasip3_0.4.0+wasi-0.3.0-rc-2026-01-06": "{\"dependencies\":[{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.10.1\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.31\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^1.3.1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"thiserror\",\"optional\":true,\"req\":\"^2.0.17\"},{\"default_features\":false,\"features\":[\"async\"],\"name\":\"wit-bindgen\",\"req\":\"^0.51.0\"},{\"default_features\":false,\"features\":[\"async-spawn\"],\"kind\":\"dev\",\"name\":\"wit-bindgen\",\"req\":\"^0.51.0\"}],\"features\":{\"http-compat\":[\"dep:bytes\",\"dep:http-body\",\"dep:http\",\"dep:thiserror\",\"wit-bindgen/async-spawn\"]}}", "wasite_0.1.0": "{\"dependencies\":[],\"features\":{}}", - "wasm-bindgen-futures_0.4.61": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.8\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.88\"},{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.111\"},{\"default_features\":false,\"features\":[\"MessageEvent\",\"Worker\"],\"name\":\"web-sys\",\"req\":\"=0.3.88\",\"target\":\"cfg(target_feature = \\\"atomics\\\")\"}],\"features\":{\"default\":[\"std\"],\"futures-core-03-stream\":[\"futures-core\"],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\",\"web-sys/std\",\"futures-util\"]}}", - "wasm-bindgen-macro-support_0.2.111": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.0.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit\",\"visit-mut\",\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.111\"}],\"features\":{\"extra-traits\":[\"syn/extra-traits\"],\"strict-macro\":[]}}", - "wasm-bindgen-macro_0.2.111": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wasm-bindgen-macro-support\",\"req\":\"=0.2.111\"}],\"features\":{\"strict-macro\":[\"wasm-bindgen-macro-support/strict-macro\"]}}", - "wasm-bindgen-shared_0.2.111": "{\"dependencies\":[{\"name\":\"unicode-ident\",\"req\":\"^1.0.5\"}],\"features\":{}}", - "wasm-bindgen_0.2.111": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"build\",\"name\":\"rustversion-compat\",\"package\":\"rustversion\",\"req\":\"^1.0.6\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"wasm-bindgen-macro\",\"req\":\"=0.2.111\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.111\"}],\"features\":{\"default\":[\"std\"],\"enable-interning\":[\"std\"],\"gg-alloc\":[],\"msrv\":[],\"rustversion\":[],\"serde-serialize\":[\"serde\",\"serde_json\",\"std\"],\"spans\":[],\"std\":[],\"strict-macro\":[\"wasm-bindgen-macro/strict-macro\"],\"xxx_debug_only_print_generated_code\":[]}}", - "wasm-encoder_0.244.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"default_features\":false,\"name\":\"leb128fmt\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2.0\"},{\"default_features\":false,\"features\":[\"simd\",\"simd\"],\"name\":\"wasmparser\",\"optional\":true,\"req\":\"^0.244.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"wasmprinter\",\"req\":\"^0.244.0\"}],\"features\":{\"component-model\":[\"wasmparser?/component-model\"],\"default\":[\"std\",\"component-model\"],\"std\":[\"wasmparser?/std\"]}}", - "wasm-metadata_0.244.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"name\":\"auditable-serde\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.0.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"indexmap\",\"req\":\"^2.7.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"spdx\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"features\":[\"std\",\"component-model\"],\"name\":\"wasm-encoder\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"component-model\",\"hash-collections\"],\"name\":\"wasmparser\",\"req\":\"^0.244.0\"}],\"features\":{\"default\":[\"oci\",\"serde\"],\"oci\":[\"dep:auditable-serde\",\"dep:flate2\",\"dep:url\",\"dep:spdx\",\"dep:serde_json\",\"serde\"],\"serde\":[\"dep:serde_derive\",\"dep:serde\"]}}", + "wasm-bindgen-futures_0.4.58": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.8\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.85\"},{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"},{\"default_features\":false,\"features\":[\"MessageEvent\",\"Worker\"],\"name\":\"web-sys\",\"req\":\"=0.3.85\",\"target\":\"cfg(target_feature = \\\"atomics\\\")\"}],\"features\":{\"default\":[\"std\"],\"futures-core-03-stream\":[\"futures-core\"],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\",\"web-sys/std\",\"futures-util\"]}}", + "wasm-bindgen-macro-support_0.2.108": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.0.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit\",\"visit-mut\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.108\"}],\"features\":{\"extra-traits\":[\"syn/extra-traits\"],\"strict-macro\":[]}}", + "wasm-bindgen-macro_0.2.108": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wasm-bindgen-macro-support\",\"req\":\"=0.2.108\"}],\"features\":{\"strict-macro\":[\"wasm-bindgen-macro-support/strict-macro\"]}}", + "wasm-bindgen-shared_0.2.108": "{\"dependencies\":[{\"name\":\"unicode-ident\",\"req\":\"^1.0.5\"}],\"features\":{}}", + "wasm-bindgen_0.2.108": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"build\",\"name\":\"rustversion-compat\",\"package\":\"rustversion\",\"req\":\"^1.0.6\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"wasm-bindgen-macro\",\"req\":\"=0.2.108\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"enable-interning\":[\"std\"],\"gg-alloc\":[],\"msrv\":[],\"rustversion\":[],\"serde-serialize\":[\"serde\",\"serde_json\",\"std\"],\"spans\":[],\"std\":[],\"strict-macro\":[\"wasm-bindgen-macro/strict-macro\"],\"xxx_debug_only_print_generated_code\":[]}}", "wasm-streams_0.4.2": "{\"dependencies\":[{\"features\":[\"io\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"features\":[\"futures\"],\"kind\":\"dev\",\"name\":\"gloo-timers\",\"req\":\"^0.3.0\"},{\"name\":\"js-sys\",\"req\":\"^0.3.72\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.95\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.45\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"features\":[\"AbortSignal\",\"QueuingStrategy\",\"ReadableStream\",\"ReadableStreamType\",\"ReadableWritablePair\",\"ReadableStreamByobReader\",\"ReadableStreamReaderMode\",\"ReadableStreamReadResult\",\"ReadableStreamByobRequest\",\"ReadableStreamDefaultReader\",\"ReadableByteStreamController\",\"ReadableStreamGetReaderOptions\",\"ReadableStreamDefaultController\",\"StreamPipeOptions\",\"TransformStream\",\"TransformStreamDefaultController\",\"Transformer\",\"UnderlyingSink\",\"UnderlyingSource\",\"WritableStream\",\"WritableStreamDefaultController\",\"WritableStreamDefaultWriter\"],\"name\":\"web-sys\",\"req\":\"^0.3.72\"},{\"features\":[\"console\",\"AbortSignal\",\"ErrorEvent\",\"PromiseRejectionEvent\",\"Response\",\"ReadableStream\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3.72\"}],\"features\":{}}", - "wasmparser_0.244.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"name\":\"bitflags\",\"req\":\"^2.4.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.2\"},{\"default_features\":false,\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.7.0\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"semver\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"}],\"features\":{\"component-model\":[\"dep:semver\"],\"default\":[\"std\",\"validate\",\"serde\",\"features\",\"component-model\",\"hash-collections\",\"simd\"],\"features\":[],\"hash-collections\":[\"dep:hashbrown\",\"dep:indexmap\"],\"prefer-btree-collections\":[],\"serde\":[\"dep:serde\",\"indexmap?/serde\",\"hashbrown?/serde\"],\"simd\":[],\"std\":[\"indexmap?/std\"],\"validate\":[]}}", "wayland-backend_0.3.12": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"concat-idents\",\"req\":\"^1.1\"},{\"name\":\"downcast-rs\",\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"raw-window-handle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"features\":[\"event\",\"fs\",\"net\",\"process\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"name\":\"rwh_06\",\"optional\":true,\"package\":\"raw-window-handle\",\"req\":\"^0.6.0\"},{\"name\":\"scoped-tls\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"scoped-tls\",\"req\":\"^1.0\"},{\"features\":[\"union\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.9\"},{\"name\":\"wayland-sys\",\"req\":\"^0.31.8\"}],\"features\":{\"client_system\":[\"wayland-sys/client\",\"dep:scoped-tls\"],\"dlopen\":[\"wayland-sys/dlopen\"],\"server_system\":[\"wayland-sys/server\",\"dep:scoped-tls\"]}}", "wayland-client_0.31.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"}],\"features\":{}}", "wayland-protocols-wlr_0.3.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-protocols\",\"req\":\"^0.32.10\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\",\"wayland-protocols/client\"],\"server\":[\"wayland-server\",\"wayland-protocols/server\"]}}", "wayland-protocols_0.32.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\"],\"server\":[\"wayland-server\"],\"staging\":[],\"unstable\":[]}}", "wayland-scanner_0.31.8": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.11\"},{\"name\":\"quick-xml\",\"req\":\"^0.38.3\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2\"}],\"features\":{}}", "wayland-sys_0.31.8": "{\"dependencies\":[{\"name\":\"dlib\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"memoffset\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.7\"}],\"features\":{\"client\":[\"dep:dlib\",\"dep:log\"],\"cursor\":[\"client\"],\"dlopen\":[\"once_cell\"],\"egl\":[\"client\"],\"server\":[\"libc\",\"memoffset\",\"dep:dlib\",\"dep:log\"]}}", - "web-sys_0.3.88": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.88\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.111\"}],\"features\":{\"AbortController\":[],\"AbortSignal\":[\"EventTarget\"],\"AbstractRange\":[],\"AddEventListenerOptions\":[],\"AesCbcParams\":[],\"AesCtrParams\":[],\"AesDerivedKeyParams\":[],\"AesGcmParams\":[],\"AesKeyAlgorithm\":[],\"AesKeyGenParams\":[],\"Algorithm\":[],\"AlignSetting\":[],\"AllowedBluetoothDevice\":[],\"AllowedUsbDevice\":[],\"AlphaOption\":[],\"AnalyserNode\":[\"AudioNode\",\"EventTarget\"],\"AnalyserOptions\":[],\"AngleInstancedArrays\":[],\"Animation\":[\"EventTarget\"],\"AnimationEffect\":[],\"AnimationEvent\":[\"Event\"],\"AnimationEventInit\":[],\"AnimationPlayState\":[],\"AnimationPlaybackEvent\":[\"Event\"],\"AnimationPlaybackEventInit\":[],\"AnimationPropertyDetails\":[],\"AnimationPropertyValueDetails\":[],\"AnimationTimeline\":[],\"AssignedNodesOptions\":[],\"AttestationConveyancePreference\":[],\"Attr\":[\"EventTarget\",\"Node\"],\"AttributeNameValue\":[],\"AudioBuffer\":[],\"AudioBufferOptions\":[],\"AudioBufferSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"AudioBufferSourceOptions\":[],\"AudioConfiguration\":[],\"AudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"AudioContextLatencyCategory\":[],\"AudioContextOptions\":[],\"AudioContextState\":[],\"AudioData\":[],\"AudioDataCopyToOptions\":[],\"AudioDataInit\":[],\"AudioDecoder\":[\"EventTarget\"],\"AudioDecoderConfig\":[],\"AudioDecoderInit\":[],\"AudioDecoderSupport\":[],\"AudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"AudioEncoder\":[\"EventTarget\"],\"AudioEncoderConfig\":[],\"AudioEncoderInit\":[],\"AudioEncoderSupport\":[],\"AudioListener\":[],\"AudioNode\":[\"EventTarget\"],\"AudioNodeOptions\":[],\"AudioParam\":[],\"AudioParamMap\":[],\"AudioProcessingEvent\":[\"Event\"],\"AudioSampleFormat\":[],\"AudioScheduledSourceNode\":[\"AudioNode\",\"EventTarget\"],\"AudioSinkInfo\":[],\"AudioSinkOptions\":[],\"AudioSinkType\":[],\"AudioStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"AudioTrack\":[],\"AudioTrackList\":[\"EventTarget\"],\"AudioWorklet\":[\"Worklet\"],\"AudioWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"AudioWorkletNode\":[\"AudioNode\",\"EventTarget\"],\"AudioWorkletNodeOptions\":[],\"AudioWorkletProcessor\":[],\"AuthenticationExtensionsClientInputs\":[],\"AuthenticationExtensionsClientInputsJson\":[],\"AuthenticationExtensionsClientOutputs\":[],\"AuthenticationExtensionsClientOutputsJson\":[],\"AuthenticationExtensionsDevicePublicKeyInputs\":[],\"AuthenticationExtensionsDevicePublicKeyOutputs\":[],\"AuthenticationExtensionsLargeBlobInputs\":[],\"AuthenticationExtensionsLargeBlobOutputs\":[],\"AuthenticationExtensionsPrfInputs\":[],\"AuthenticationExtensionsPrfOutputs\":[],\"AuthenticationExtensionsPrfValues\":[],\"AuthenticationResponseJson\":[],\"AuthenticatorAssertionResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAssertionResponseJson\":[],\"AuthenticatorAttachment\":[],\"AuthenticatorAttestationResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAttestationResponseJson\":[],\"AuthenticatorResponse\":[],\"AuthenticatorSelectionCriteria\":[],\"AuthenticatorTransport\":[],\"AutoKeyword\":[],\"AutocompleteInfo\":[],\"BarProp\":[],\"BaseAudioContext\":[\"EventTarget\"],\"BaseComputedKeyframe\":[],\"BaseKeyframe\":[],\"BasePropertyIndexedKeyframe\":[],\"BasicCardRequest\":[],\"BasicCardResponse\":[],\"BasicCardType\":[],\"BatteryManager\":[\"EventTarget\"],\"BeforeUnloadEvent\":[\"Event\"],\"BinaryType\":[],\"BiquadFilterNode\":[\"AudioNode\",\"EventTarget\"],\"BiquadFilterOptions\":[],\"BiquadFilterType\":[],\"BitrateMode\":[],\"Blob\":[],\"BlobEvent\":[\"Event\"],\"BlobEventInit\":[],\"BlobPropertyBag\":[],\"BlockParsingOptions\":[],\"Bluetooth\":[\"EventTarget\"],\"BluetoothAdvertisingEvent\":[\"Event\"],\"BluetoothAdvertisingEventInit\":[],\"BluetoothCharacteristicProperties\":[],\"BluetoothDataFilterInit\":[],\"BluetoothDevice\":[\"EventTarget\"],\"BluetoothLeScanFilterInit\":[],\"BluetoothManufacturerDataMap\":[],\"BluetoothPermissionDescriptor\":[],\"BluetoothPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"BluetoothPermissionStorage\":[],\"BluetoothRemoteGattCharacteristic\":[\"EventTarget\"],\"BluetoothRemoteGattDescriptor\":[],\"BluetoothRemoteGattServer\":[],\"BluetoothRemoteGattService\":[\"EventTarget\"],\"BluetoothServiceDataMap\":[],\"BluetoothUuid\":[],\"BoxQuadOptions\":[],\"BroadcastChannel\":[\"EventTarget\"],\"BrowserElementDownloadOptions\":[],\"BrowserElementExecuteScriptOptions\":[],\"BrowserFeedWriter\":[],\"BrowserFindCaseSensitivity\":[],\"BrowserFindDirection\":[],\"ByteLengthQueuingStrategy\":[],\"Cache\":[],\"CacheBatchOperation\":[],\"CacheQueryOptions\":[],\"CacheStorage\":[],\"CacheStorageNamespace\":[],\"CanvasCaptureMediaStream\":[\"EventTarget\",\"MediaStream\"],\"CanvasCaptureMediaStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"CanvasGradient\":[],\"CanvasPattern\":[],\"CanvasRenderingContext2d\":[],\"CanvasWindingRule\":[],\"CaretChangedReason\":[],\"CaretPosition\":[],\"CaretStateChangedEventInit\":[],\"CdataSection\":[\"CharacterData\",\"EventTarget\",\"Node\",\"Text\"],\"ChannelCountMode\":[],\"ChannelInterpretation\":[],\"ChannelMergerNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelMergerOptions\":[],\"ChannelSplitterNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelSplitterOptions\":[],\"CharacterData\":[\"EventTarget\",\"Node\"],\"CheckerboardReason\":[],\"CheckerboardReport\":[],\"CheckerboardReportService\":[],\"ChromeFilePropertyBag\":[],\"ChromeWorker\":[\"EventTarget\",\"Worker\"],\"Client\":[],\"ClientQueryOptions\":[],\"ClientRectsAndTexts\":[],\"ClientType\":[],\"Clients\":[],\"Clipboard\":[\"EventTarget\"],\"ClipboardEvent\":[\"Event\"],\"ClipboardEventInit\":[],\"ClipboardItem\":[],\"ClipboardItemOptions\":[],\"ClipboardPermissionDescriptor\":[],\"ClipboardUnsanitizedFormats\":[],\"CloseEvent\":[\"Event\"],\"CloseEventInit\":[],\"CodecState\":[],\"CollectedClientData\":[],\"ColorSpaceConversion\":[],\"CommandEvent\":[\"Event\"],\"CommandEventInit\":[],\"Comment\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"CompositeOperation\":[],\"CompositionEvent\":[\"Event\",\"UiEvent\"],\"CompositionEventInit\":[],\"CompressionFormat\":[],\"CompressionStream\":[],\"ComputedEffectTiming\":[],\"ConnStatusDict\":[],\"ConnectionType\":[],\"ConsoleCounter\":[],\"ConsoleCounterError\":[],\"ConsoleEvent\":[],\"ConsoleInstance\":[],\"ConsoleInstanceOptions\":[],\"ConsoleLevel\":[],\"ConsoleLogLevel\":[],\"ConsoleProfileEvent\":[],\"ConsoleStackEntry\":[],\"ConsoleTimerError\":[],\"ConsoleTimerLogOrEnd\":[],\"ConsoleTimerStart\":[],\"ConstantSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"ConstantSourceOptions\":[],\"ConstrainBooleanParameters\":[],\"ConstrainDomStringParameters\":[],\"ConstrainDoubleRange\":[],\"ConstrainLongRange\":[],\"ContextAttributes2d\":[],\"ConvertCoordinateOptions\":[],\"ConvolverNode\":[\"AudioNode\",\"EventTarget\"],\"ConvolverOptions\":[],\"CookieChangeEvent\":[\"Event\"],\"CookieChangeEventInit\":[],\"CookieInit\":[],\"CookieListItem\":[],\"CookieSameSite\":[],\"CookieStore\":[\"EventTarget\"],\"CookieStoreDeleteOptions\":[],\"CookieStoreGetOptions\":[],\"CookieStoreManager\":[],\"Coordinates\":[],\"CountQueuingStrategy\":[],\"Credential\":[],\"CredentialCreationOptions\":[],\"CredentialPropertiesOutput\":[],\"CredentialRequestOptions\":[],\"CredentialsContainer\":[],\"Crypto\":[],\"CryptoKey\":[],\"CryptoKeyPair\":[],\"CssAnimation\":[\"Animation\",\"EventTarget\"],\"CssBoxType\":[],\"CssConditionRule\":[\"CssGroupingRule\",\"CssRule\"],\"CssCounterStyleRule\":[\"CssRule\"],\"CssFontFaceRule\":[\"CssRule\"],\"CssFontFeatureValuesRule\":[\"CssRule\"],\"CssGroupingRule\":[\"CssRule\"],\"CssImportRule\":[\"CssRule\"],\"CssKeyframeRule\":[\"CssRule\"],\"CssKeyframesRule\":[\"CssRule\"],\"CssMediaRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssNamespaceRule\":[\"CssRule\"],\"CssPageRule\":[\"CssRule\"],\"CssPseudoElement\":[],\"CssRule\":[],\"CssRuleList\":[],\"CssStyleDeclaration\":[],\"CssStyleRule\":[\"CssRule\"],\"CssStyleSheet\":[\"StyleSheet\"],\"CssStyleSheetParsingMode\":[],\"CssSupportsRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssTransition\":[\"Animation\",\"EventTarget\"],\"CustomElementRegistry\":[],\"CustomEvent\":[\"Event\"],\"CustomEventInit\":[],\"DataTransfer\":[],\"DataTransferItem\":[],\"DataTransferItemList\":[],\"DateTimeValue\":[],\"DecoderDoctorNotification\":[],\"DecoderDoctorNotificationType\":[],\"DecompressionStream\":[],\"DedicatedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"DelayNode\":[\"AudioNode\",\"EventTarget\"],\"DelayOptions\":[],\"DeviceAcceleration\":[],\"DeviceAccelerationInit\":[],\"DeviceLightEvent\":[\"Event\"],\"DeviceLightEventInit\":[],\"DeviceMotionEvent\":[\"Event\"],\"DeviceMotionEventInit\":[],\"DeviceOrientationEvent\":[\"Event\"],\"DeviceOrientationEventInit\":[],\"DeviceProximityEvent\":[\"Event\"],\"DeviceProximityEventInit\":[],\"DeviceRotationRate\":[],\"DeviceRotationRateInit\":[],\"DhKeyDeriveParams\":[],\"DirectionSetting\":[],\"Directory\":[],\"DirectoryPickerOptions\":[],\"DisplayMediaStreamConstraints\":[],\"DisplayNameOptions\":[],\"DisplayNameResult\":[],\"DistanceModelType\":[],\"DnsCacheDict\":[],\"DnsCacheEntry\":[],\"DnsLookupDict\":[],\"Document\":[\"EventTarget\",\"Node\"],\"DocumentFragment\":[\"EventTarget\",\"Node\"],\"DocumentTimeline\":[\"AnimationTimeline\"],\"DocumentTimelineOptions\":[],\"DocumentType\":[\"EventTarget\",\"Node\"],\"DomError\":[],\"DomException\":[],\"DomImplementation\":[],\"DomMatrix\":[\"DomMatrixReadOnly\"],\"DomMatrix2dInit\":[],\"DomMatrixInit\":[],\"DomMatrixReadOnly\":[],\"DomParser\":[],\"DomPoint\":[\"DomPointReadOnly\"],\"DomPointInit\":[],\"DomPointReadOnly\":[],\"DomQuad\":[],\"DomQuadInit\":[],\"DomQuadJson\":[],\"DomRect\":[\"DomRectReadOnly\"],\"DomRectInit\":[],\"DomRectList\":[],\"DomRectReadOnly\":[],\"DomRequest\":[\"EventTarget\"],\"DomRequestReadyState\":[],\"DomStringList\":[],\"DomStringMap\":[],\"DomTokenList\":[],\"DomWindowResizeEventDetail\":[],\"DoubleRange\":[],\"DragEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"DragEventInit\":[],\"DynamicsCompressorNode\":[\"AudioNode\",\"EventTarget\"],\"DynamicsCompressorOptions\":[],\"EcKeyAlgorithm\":[],\"EcKeyGenParams\":[],\"EcKeyImportParams\":[],\"EcdhKeyDeriveParams\":[],\"EcdsaParams\":[],\"EffectTiming\":[],\"Element\":[\"EventTarget\",\"Node\"],\"ElementCreationOptions\":[],\"ElementDefinitionOptions\":[],\"EncodedAudioChunk\":[],\"EncodedAudioChunkInit\":[],\"EncodedAudioChunkMetadata\":[],\"EncodedAudioChunkType\":[],\"EncodedVideoChunk\":[],\"EncodedVideoChunkInit\":[],\"EncodedVideoChunkMetadata\":[],\"EncodedVideoChunkType\":[],\"EndingTypes\":[],\"ErrorCallback\":[],\"ErrorEvent\":[\"Event\"],\"ErrorEventInit\":[],\"Event\":[],\"EventInit\":[],\"EventListener\":[],\"EventListenerOptions\":[],\"EventModifierInit\":[],\"EventSource\":[\"EventTarget\"],\"EventSourceInit\":[],\"EventTarget\":[],\"Exception\":[],\"ExtBlendMinmax\":[],\"ExtColorBufferFloat\":[],\"ExtColorBufferHalfFloat\":[],\"ExtDisjointTimerQuery\":[],\"ExtFragDepth\":[],\"ExtSRgb\":[],\"ExtShaderTextureLod\":[],\"ExtTextureFilterAnisotropic\":[],\"ExtTextureNorm16\":[],\"ExtendableCookieChangeEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableCookieChangeEventInit\":[],\"ExtendableEvent\":[\"Event\"],\"ExtendableEventInit\":[],\"ExtendableMessageEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableMessageEventInit\":[],\"External\":[],\"FakePluginMimeEntry\":[],\"FakePluginTagInit\":[],\"FetchEvent\":[\"Event\",\"ExtendableEvent\"],\"FetchEventInit\":[],\"FetchObserver\":[\"EventTarget\"],\"FetchReadableStreamReadDataArray\":[],\"FetchReadableStreamReadDataDone\":[],\"FetchState\":[],\"File\":[\"Blob\"],\"FileCallback\":[],\"FileList\":[],\"FilePickerAcceptType\":[],\"FilePickerOptions\":[],\"FilePropertyBag\":[],\"FileReader\":[\"EventTarget\"],\"FileReaderSync\":[],\"FileSystem\":[],\"FileSystemCreateWritableOptions\":[],\"FileSystemDirectoryEntry\":[\"FileSystemEntry\"],\"FileSystemDirectoryHandle\":[\"FileSystemHandle\"],\"FileSystemDirectoryReader\":[],\"FileSystemEntriesCallback\":[],\"FileSystemEntry\":[],\"FileSystemEntryCallback\":[],\"FileSystemFileEntry\":[\"FileSystemEntry\"],\"FileSystemFileHandle\":[\"FileSystemHandle\"],\"FileSystemFlags\":[],\"FileSystemGetDirectoryOptions\":[],\"FileSystemGetFileOptions\":[],\"FileSystemHandle\":[],\"FileSystemHandleKind\":[],\"FileSystemHandlePermissionDescriptor\":[],\"FileSystemPermissionDescriptor\":[],\"FileSystemPermissionMode\":[],\"FileSystemReadWriteOptions\":[],\"FileSystemRemoveOptions\":[],\"FileSystemSyncAccessHandle\":[],\"FileSystemSyncAccessHandleMode\":[],\"FileSystemSyncAccessHandleOptions\":[],\"FileSystemWritableFileStream\":[\"WritableStream\"],\"FillLightMode\":[],\"FillMode\":[],\"FlashClassification\":[],\"FlowControlType\":[],\"FocusEvent\":[\"Event\",\"UiEvent\"],\"FocusEventInit\":[],\"FocusOptions\":[],\"FontData\":[],\"FontFace\":[],\"FontFaceDescriptors\":[],\"FontFaceLoadStatus\":[],\"FontFaceSet\":[\"EventTarget\"],\"FontFaceSetIterator\":[],\"FontFaceSetIteratorResult\":[],\"FontFaceSetLoadEvent\":[\"Event\"],\"FontFaceSetLoadEventInit\":[],\"FontFaceSetLoadStatus\":[],\"FormData\":[],\"FrameType\":[],\"FuzzingFunctions\":[],\"GainNode\":[\"AudioNode\",\"EventTarget\"],\"GainOptions\":[],\"Gamepad\":[],\"GamepadButton\":[],\"GamepadEffectParameters\":[],\"GamepadEvent\":[\"Event\"],\"GamepadEventInit\":[],\"GamepadHand\":[],\"GamepadHapticActuator\":[],\"GamepadHapticActuatorType\":[],\"GamepadHapticEffectType\":[],\"GamepadHapticsResult\":[],\"GamepadMappingType\":[],\"GamepadPose\":[],\"GamepadTouch\":[],\"Geolocation\":[],\"GeolocationCoordinates\":[],\"GeolocationPosition\":[],\"GeolocationPositionError\":[],\"GestureEvent\":[\"Event\",\"UiEvent\"],\"GetAnimationsOptions\":[],\"GetRootNodeOptions\":[],\"GetUserMediaRequest\":[],\"Gpu\":[],\"GpuAdapter\":[],\"GpuAdapterInfo\":[],\"GpuAddressMode\":[],\"GpuAutoLayoutMode\":[],\"GpuBindGroup\":[],\"GpuBindGroupDescriptor\":[],\"GpuBindGroupEntry\":[],\"GpuBindGroupLayout\":[],\"GpuBindGroupLayoutDescriptor\":[],\"GpuBindGroupLayoutEntry\":[],\"GpuBlendComponent\":[],\"GpuBlendFactor\":[],\"GpuBlendOperation\":[],\"GpuBlendState\":[],\"GpuBuffer\":[],\"GpuBufferBinding\":[],\"GpuBufferBindingLayout\":[],\"GpuBufferBindingType\":[],\"GpuBufferDescriptor\":[],\"GpuBufferMapState\":[],\"GpuCanvasAlphaMode\":[],\"GpuCanvasConfiguration\":[],\"GpuCanvasContext\":[],\"GpuCanvasToneMapping\":[],\"GpuCanvasToneMappingMode\":[],\"GpuColorDict\":[],\"GpuColorTargetState\":[],\"GpuCommandBuffer\":[],\"GpuCommandBufferDescriptor\":[],\"GpuCommandEncoder\":[],\"GpuCommandEncoderDescriptor\":[],\"GpuCompareFunction\":[],\"GpuCompilationInfo\":[],\"GpuCompilationMessage\":[],\"GpuCompilationMessageType\":[],\"GpuComputePassDescriptor\":[],\"GpuComputePassEncoder\":[],\"GpuComputePassTimestampWrites\":[],\"GpuComputePipeline\":[],\"GpuComputePipelineDescriptor\":[],\"GpuCopyExternalImageDestInfo\":[],\"GpuCopyExternalImageSourceInfo\":[],\"GpuCullMode\":[],\"GpuDepthStencilState\":[],\"GpuDevice\":[\"EventTarget\"],\"GpuDeviceDescriptor\":[],\"GpuDeviceLostInfo\":[],\"GpuDeviceLostReason\":[],\"GpuError\":[],\"GpuErrorFilter\":[],\"GpuExtent3dDict\":[],\"GpuExternalTexture\":[],\"GpuExternalTextureBindingLayout\":[],\"GpuExternalTextureDescriptor\":[],\"GpuFeatureName\":[],\"GpuFilterMode\":[],\"GpuFragmentState\":[],\"GpuFrontFace\":[],\"GpuIndexFormat\":[],\"GpuInternalError\":[\"GpuError\"],\"GpuLoadOp\":[],\"GpuMipmapFilterMode\":[],\"GpuMultisampleState\":[],\"GpuObjectDescriptorBase\":[],\"GpuOrigin2dDict\":[],\"GpuOrigin3dDict\":[],\"GpuOutOfMemoryError\":[\"GpuError\"],\"GpuPipelineDescriptorBase\":[],\"GpuPipelineError\":[\"DomException\"],\"GpuPipelineErrorInit\":[],\"GpuPipelineErrorReason\":[],\"GpuPipelineLayout\":[],\"GpuPipelineLayoutDescriptor\":[],\"GpuPowerPreference\":[],\"GpuPrimitiveState\":[],\"GpuPrimitiveTopology\":[],\"GpuProgrammableStage\":[],\"GpuQuerySet\":[],\"GpuQuerySetDescriptor\":[],\"GpuQueryType\":[],\"GpuQueue\":[],\"GpuQueueDescriptor\":[],\"GpuRenderBundle\":[],\"GpuRenderBundleDescriptor\":[],\"GpuRenderBundleEncoder\":[],\"GpuRenderBundleEncoderDescriptor\":[],\"GpuRenderPassColorAttachment\":[],\"GpuRenderPassDepthStencilAttachment\":[],\"GpuRenderPassDescriptor\":[],\"GpuRenderPassEncoder\":[],\"GpuRenderPassLayout\":[],\"GpuRenderPassTimestampWrites\":[],\"GpuRenderPipeline\":[],\"GpuRenderPipelineDescriptor\":[],\"GpuRequestAdapterOptions\":[],\"GpuSampler\":[],\"GpuSamplerBindingLayout\":[],\"GpuSamplerBindingType\":[],\"GpuSamplerDescriptor\":[],\"GpuShaderModule\":[],\"GpuShaderModuleCompilationHint\":[],\"GpuShaderModuleDescriptor\":[],\"GpuStencilFaceState\":[],\"GpuStencilOperation\":[],\"GpuStorageTextureAccess\":[],\"GpuStorageTextureBindingLayout\":[],\"GpuStoreOp\":[],\"GpuSupportedFeatures\":[],\"GpuSupportedLimits\":[],\"GpuTexelCopyBufferInfo\":[],\"GpuTexelCopyBufferLayout\":[],\"GpuTexelCopyTextureInfo\":[],\"GpuTexture\":[],\"GpuTextureAspect\":[],\"GpuTextureBindingLayout\":[],\"GpuTextureDescriptor\":[],\"GpuTextureDimension\":[],\"GpuTextureFormat\":[],\"GpuTextureSampleType\":[],\"GpuTextureView\":[],\"GpuTextureViewDescriptor\":[],\"GpuTextureViewDimension\":[],\"GpuUncapturedErrorEvent\":[\"Event\"],\"GpuUncapturedErrorEventInit\":[],\"GpuValidationError\":[\"GpuError\"],\"GpuVertexAttribute\":[],\"GpuVertexBufferLayout\":[],\"GpuVertexFormat\":[],\"GpuVertexState\":[],\"GpuVertexStepMode\":[],\"GroupedHistoryEventInit\":[],\"HalfOpenInfoDict\":[],\"HardwareAcceleration\":[],\"HashChangeEvent\":[\"Event\"],\"HashChangeEventInit\":[],\"Headers\":[],\"HeadersGuardEnum\":[],\"Hid\":[\"EventTarget\"],\"HidCollectionInfo\":[],\"HidConnectionEvent\":[\"Event\"],\"HidConnectionEventInit\":[],\"HidDevice\":[\"EventTarget\"],\"HidDeviceFilter\":[],\"HidDeviceRequestOptions\":[],\"HidInputReportEvent\":[\"Event\"],\"HidInputReportEventInit\":[],\"HidReportInfo\":[],\"HidReportItem\":[],\"HidUnitSystem\":[],\"HiddenPluginEventInit\":[],\"Highlight\":[],\"HighlightHitResult\":[],\"HighlightRegistry\":[],\"HighlightType\":[],\"HighlightsFromPointOptions\":[],\"History\":[],\"HitRegionOptions\":[],\"HkdfParams\":[],\"HmacDerivedKeyParams\":[],\"HmacImportParams\":[],\"HmacKeyAlgorithm\":[],\"HmacKeyGenParams\":[],\"HtmlAllCollection\":[],\"HtmlAnchorElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAudioElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HtmlBaseElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBodyElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlButtonElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCanvasElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCollection\":[],\"HtmlDListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDetailsElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDialogElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDirectoryElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDivElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"HtmlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"HtmlEmbedElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFieldSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFontElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFormControlsCollection\":[\"HtmlCollection\"],\"HtmlFormElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadingElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHtmlElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlIFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlImageElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlInputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLabelElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLegendElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLiElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLinkElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMapElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMediaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuItemElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMetaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMeterElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlModElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlObjectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptGroupElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionsCollection\":[\"HtmlCollection\"],\"HtmlOutputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParagraphElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParamElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPictureElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPreElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlProgressElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlQuoteElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlScriptElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSelectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSlotElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSourceElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSpanElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlStyleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCaptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCellElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableColElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableRowElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableSectionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTemplateElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTextAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTimeElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTitleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTrackElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUnknownElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlVideoElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HttpConnDict\":[],\"HttpConnInfo\":[],\"HttpConnectionElement\":[],\"IdbCursor\":[],\"IdbCursorDirection\":[],\"IdbCursorWithValue\":[\"IdbCursor\"],\"IdbDatabase\":[\"EventTarget\"],\"IdbFactory\":[],\"IdbFileHandle\":[\"EventTarget\"],\"IdbFileMetadataParameters\":[],\"IdbFileRequest\":[\"DomRequest\",\"EventTarget\"],\"IdbIndex\":[],\"IdbIndexParameters\":[],\"IdbKeyRange\":[],\"IdbLocaleAwareKeyRange\":[\"IdbKeyRange\"],\"IdbMutableFile\":[\"EventTarget\"],\"IdbObjectStore\":[],\"IdbObjectStoreParameters\":[],\"IdbOpenDbOptions\":[],\"IdbOpenDbRequest\":[\"EventTarget\",\"IdbRequest\"],\"IdbRequest\":[\"EventTarget\"],\"IdbRequestReadyState\":[],\"IdbTransaction\":[\"EventTarget\"],\"IdbTransactionDurability\":[],\"IdbTransactionMode\":[],\"IdbTransactionOptions\":[],\"IdbVersionChangeEvent\":[\"Event\"],\"IdbVersionChangeEventInit\":[],\"IdleDeadline\":[],\"IdleRequestOptions\":[],\"IirFilterNode\":[\"AudioNode\",\"EventTarget\"],\"IirFilterOptions\":[],\"ImageBitmap\":[],\"ImageBitmapOptions\":[],\"ImageBitmapRenderingContext\":[],\"ImageCapture\":[],\"ImageCaptureError\":[],\"ImageCaptureErrorEvent\":[\"Event\"],\"ImageCaptureErrorEventInit\":[],\"ImageData\":[],\"ImageDecodeOptions\":[],\"ImageDecodeResult\":[],\"ImageDecoder\":[],\"ImageDecoderInit\":[],\"ImageEncodeOptions\":[],\"ImageOrientation\":[],\"ImageTrack\":[],\"ImageTrackList\":[],\"InputDeviceInfo\":[\"MediaDeviceInfo\"],\"InputEvent\":[\"Event\",\"UiEvent\"],\"InputEventInit\":[],\"IntersectionObserver\":[],\"IntersectionObserverEntry\":[],\"IntersectionObserverEntryInit\":[],\"IntersectionObserverInit\":[],\"IntlUtils\":[],\"IsInputPendingOptions\":[],\"IterableKeyAndValueResult\":[],\"IterableKeyOrValueResult\":[],\"IterationCompositeOperation\":[],\"JsonWebKey\":[],\"KeyAlgorithm\":[],\"KeyEvent\":[],\"KeyFrameRequestEvent\":[\"Event\"],\"KeyIdsInitData\":[],\"KeyboardEvent\":[\"Event\",\"UiEvent\"],\"KeyboardEventInit\":[],\"KeyframeAnimationOptions\":[],\"KeyframeEffect\":[\"AnimationEffect\"],\"KeyframeEffectOptions\":[],\"L10nElement\":[],\"L10nValue\":[],\"LargeBlobSupport\":[],\"LatencyMode\":[],\"LifecycleCallbacks\":[],\"LineAlignSetting\":[],\"ListBoxObject\":[],\"LocalMediaStream\":[\"EventTarget\",\"MediaStream\"],\"LocaleInfo\":[],\"Location\":[],\"Lock\":[],\"LockInfo\":[],\"LockManager\":[],\"LockManagerSnapshot\":[],\"LockMode\":[],\"LockOptions\":[],\"MathMlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"MediaCapabilities\":[],\"MediaCapabilitiesInfo\":[],\"MediaConfiguration\":[],\"MediaDecodingConfiguration\":[],\"MediaDecodingType\":[],\"MediaDeviceInfo\":[],\"MediaDeviceKind\":[],\"MediaDevices\":[\"EventTarget\"],\"MediaElementAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaElementAudioSourceOptions\":[],\"MediaEncodingConfiguration\":[],\"MediaEncodingType\":[],\"MediaEncryptedEvent\":[\"Event\"],\"MediaError\":[],\"MediaImage\":[],\"MediaKeyError\":[\"Event\"],\"MediaKeyMessageEvent\":[\"Event\"],\"MediaKeyMessageEventInit\":[],\"MediaKeyMessageType\":[],\"MediaKeyNeededEventInit\":[],\"MediaKeySession\":[\"EventTarget\"],\"MediaKeySessionType\":[],\"MediaKeyStatus\":[],\"MediaKeyStatusMap\":[],\"MediaKeySystemAccess\":[],\"MediaKeySystemConfiguration\":[],\"MediaKeySystemMediaCapability\":[],\"MediaKeySystemStatus\":[],\"MediaKeys\":[],\"MediaKeysPolicy\":[],\"MediaKeysRequirement\":[],\"MediaList\":[],\"MediaMetadata\":[],\"MediaMetadataInit\":[],\"MediaPositionState\":[],\"MediaQueryList\":[\"EventTarget\"],\"MediaQueryListEvent\":[\"Event\"],\"MediaQueryListEventInit\":[],\"MediaRecorder\":[\"EventTarget\"],\"MediaRecorderErrorEvent\":[\"Event\"],\"MediaRecorderErrorEventInit\":[],\"MediaRecorderOptions\":[],\"MediaSession\":[],\"MediaSessionAction\":[],\"MediaSessionActionDetails\":[],\"MediaSessionPlaybackState\":[],\"MediaSettingsRange\":[],\"MediaSource\":[\"EventTarget\"],\"MediaSourceEndOfStreamError\":[],\"MediaSourceEnum\":[],\"MediaSourceReadyState\":[],\"MediaStream\":[\"EventTarget\"],\"MediaStreamAudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceOptions\":[],\"MediaStreamConstraints\":[],\"MediaStreamError\":[],\"MediaStreamEvent\":[\"Event\"],\"MediaStreamEventInit\":[],\"MediaStreamTrack\":[\"EventTarget\"],\"MediaStreamTrackEvent\":[\"Event\"],\"MediaStreamTrackEventInit\":[],\"MediaStreamTrackGenerator\":[\"EventTarget\",\"MediaStreamTrack\"],\"MediaStreamTrackGeneratorInit\":[],\"MediaStreamTrackProcessor\":[],\"MediaStreamTrackProcessorInit\":[],\"MediaStreamTrackState\":[],\"MediaTrackCapabilities\":[],\"MediaTrackConstraintSet\":[],\"MediaTrackConstraints\":[],\"MediaTrackSettings\":[],\"MediaTrackSupportedConstraints\":[],\"MemoryAttribution\":[],\"MemoryAttributionContainer\":[],\"MemoryBreakdownEntry\":[],\"MemoryMeasurement\":[],\"MessageChannel\":[],\"MessageEvent\":[\"Event\"],\"MessageEventInit\":[],\"MessagePort\":[\"EventTarget\"],\"MeteringMode\":[],\"MidiAccess\":[\"EventTarget\"],\"MidiConnectionEvent\":[\"Event\"],\"MidiConnectionEventInit\":[],\"MidiInput\":[\"EventTarget\",\"MidiPort\"],\"MidiInputMap\":[],\"MidiMessageEvent\":[\"Event\"],\"MidiMessageEventInit\":[],\"MidiOptions\":[],\"MidiOutput\":[\"EventTarget\",\"MidiPort\"],\"MidiOutputMap\":[],\"MidiPort\":[\"EventTarget\"],\"MidiPortConnectionState\":[],\"MidiPortDeviceState\":[],\"MidiPortType\":[],\"MimeType\":[],\"MimeTypeArray\":[],\"MouseEvent\":[\"Event\",\"UiEvent\"],\"MouseEventInit\":[],\"MouseScrollEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"MozDebug\":[],\"MutationEvent\":[\"Event\"],\"MutationObserver\":[],\"MutationObserverInit\":[],\"MutationObservingInfo\":[],\"MutationRecord\":[],\"NamedNodeMap\":[],\"NativeOsFileReadOptions\":[],\"NativeOsFileWriteAtomicOptions\":[],\"NavigationType\":[],\"Navigator\":[],\"NavigatorAutomationInformation\":[],\"NavigatorUaBrandVersion\":[],\"NavigatorUaData\":[],\"NetworkCommandOptions\":[],\"NetworkInformation\":[\"EventTarget\"],\"NetworkResultOptions\":[],\"Node\":[\"EventTarget\"],\"NodeFilter\":[],\"NodeIterator\":[],\"NodeList\":[],\"Notification\":[\"EventTarget\"],\"NotificationAction\":[],\"NotificationDirection\":[],\"NotificationEvent\":[\"Event\",\"ExtendableEvent\"],\"NotificationEventInit\":[],\"NotificationOptions\":[],\"NotificationPermission\":[],\"ObserverCallback\":[],\"OesElementIndexUint\":[],\"OesStandardDerivatives\":[],\"OesTextureFloat\":[],\"OesTextureFloatLinear\":[],\"OesTextureHalfFloat\":[],\"OesTextureHalfFloatLinear\":[],\"OesVertexArrayObject\":[],\"OfflineAudioCompletionEvent\":[\"Event\"],\"OfflineAudioCompletionEventInit\":[],\"OfflineAudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"OfflineAudioContextOptions\":[],\"OfflineResourceList\":[\"EventTarget\"],\"OffscreenCanvas\":[\"EventTarget\"],\"OffscreenCanvasRenderingContext2d\":[],\"OpenFilePickerOptions\":[],\"OpenWindowEventDetail\":[],\"OptionalEffectTiming\":[],\"OrientationLockType\":[],\"OrientationType\":[],\"OscillatorNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"OscillatorOptions\":[],\"OscillatorType\":[],\"OverSampleType\":[],\"OvrMultiview2\":[],\"PageTransitionEvent\":[\"Event\"],\"PageTransitionEventInit\":[],\"PaintRequest\":[],\"PaintRequestList\":[],\"PaintWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"PannerNode\":[\"AudioNode\",\"EventTarget\"],\"PannerOptions\":[],\"PanningModelType\":[],\"ParityType\":[],\"Path2d\":[],\"PaymentAddress\":[],\"PaymentComplete\":[],\"PaymentMethodChangeEvent\":[\"Event\",\"PaymentRequestUpdateEvent\"],\"PaymentMethodChangeEventInit\":[],\"PaymentRequestUpdateEvent\":[\"Event\"],\"PaymentRequestUpdateEventInit\":[],\"PaymentResponse\":[],\"Pbkdf2Params\":[],\"PcImplIceConnectionState\":[],\"PcImplIceGatheringState\":[],\"PcImplSignalingState\":[],\"PcObserverStateType\":[],\"Performance\":[\"EventTarget\"],\"PerformanceEntry\":[],\"PerformanceEntryEventInit\":[],\"PerformanceEntryFilterOptions\":[],\"PerformanceMark\":[\"PerformanceEntry\"],\"PerformanceMarkOptions\":[],\"PerformanceMeasure\":[\"PerformanceEntry\"],\"PerformanceMeasureOptions\":[],\"PerformanceNavigation\":[],\"PerformanceNavigationTiming\":[\"PerformanceEntry\",\"PerformanceResourceTiming\"],\"PerformanceObserver\":[],\"PerformanceObserverEntryList\":[],\"PerformanceObserverInit\":[],\"PerformanceResourceTiming\":[\"PerformanceEntry\"],\"PerformanceServerTiming\":[],\"PerformanceTiming\":[],\"PeriodicWave\":[],\"PeriodicWaveConstraints\":[],\"PeriodicWaveOptions\":[],\"PermissionDescriptor\":[],\"PermissionName\":[],\"PermissionState\":[],\"PermissionStatus\":[\"EventTarget\"],\"Permissions\":[],\"PhotoCapabilities\":[],\"PhotoSettings\":[],\"PictureInPictureEvent\":[\"Event\"],\"PictureInPictureEventInit\":[],\"PictureInPictureWindow\":[\"EventTarget\"],\"PlaneLayout\":[],\"PlaybackDirection\":[],\"Plugin\":[],\"PluginArray\":[],\"PluginCrashedEventInit\":[],\"Point2d\":[],\"PointerEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"PointerEventInit\":[],\"PopStateEvent\":[\"Event\"],\"PopStateEventInit\":[],\"PopupBlockedEvent\":[\"Event\"],\"PopupBlockedEventInit\":[],\"Position\":[],\"PositionAlignSetting\":[],\"PositionError\":[],\"PositionOptions\":[],\"PremultiplyAlpha\":[],\"Presentation\":[],\"PresentationAvailability\":[\"EventTarget\"],\"PresentationConnection\":[\"EventTarget\"],\"PresentationConnectionAvailableEvent\":[\"Event\"],\"PresentationConnectionAvailableEventInit\":[],\"PresentationConnectionBinaryType\":[],\"PresentationConnectionCloseEvent\":[\"Event\"],\"PresentationConnectionCloseEventInit\":[],\"PresentationConnectionClosedReason\":[],\"PresentationConnectionList\":[\"EventTarget\"],\"PresentationConnectionState\":[],\"PresentationReceiver\":[],\"PresentationRequest\":[\"EventTarget\"],\"PresentationStyle\":[],\"ProcessingInstruction\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"ProfileTimelineLayerRect\":[],\"ProfileTimelineMarker\":[],\"ProfileTimelineMessagePortOperationType\":[],\"ProfileTimelineStackFrame\":[],\"ProfileTimelineWorkerOperationType\":[],\"ProgressEvent\":[\"Event\"],\"ProgressEventInit\":[],\"PromiseNativeHandler\":[],\"PromiseRejectionEvent\":[\"Event\"],\"PromiseRejectionEventInit\":[],\"PublicKeyCredential\":[\"Credential\"],\"PublicKeyCredentialCreationOptions\":[],\"PublicKeyCredentialCreationOptionsJson\":[],\"PublicKeyCredentialDescriptor\":[],\"PublicKeyCredentialDescriptorJson\":[],\"PublicKeyCredentialEntity\":[],\"PublicKeyCredentialHints\":[],\"PublicKeyCredentialParameters\":[],\"PublicKeyCredentialRequestOptions\":[],\"PublicKeyCredentialRequestOptionsJson\":[],\"PublicKeyCredentialRpEntity\":[],\"PublicKeyCredentialType\":[],\"PublicKeyCredentialUserEntity\":[],\"PublicKeyCredentialUserEntityJson\":[],\"PushEncryptionKeyName\":[],\"PushEvent\":[\"Event\",\"ExtendableEvent\"],\"PushEventInit\":[],\"PushManager\":[],\"PushMessageData\":[],\"PushPermissionState\":[],\"PushSubscription\":[],\"PushSubscriptionInit\":[],\"PushSubscriptionJson\":[],\"PushSubscriptionKeys\":[],\"PushSubscriptionOptions\":[],\"PushSubscriptionOptionsInit\":[],\"QueryOptions\":[],\"QueuingStrategy\":[],\"QueuingStrategyInit\":[],\"RadioNodeList\":[\"NodeList\"],\"Range\":[\"AbstractRange\"],\"RcwnPerfStats\":[],\"RcwnStatus\":[],\"ReadableByteStreamController\":[],\"ReadableStream\":[],\"ReadableStreamByobReader\":[],\"ReadableStreamByobRequest\":[],\"ReadableStreamDefaultController\":[],\"ReadableStreamDefaultReader\":[],\"ReadableStreamGetReaderOptions\":[],\"ReadableStreamIteratorOptions\":[],\"ReadableStreamReadResult\":[],\"ReadableStreamReaderMode\":[],\"ReadableStreamType\":[],\"ReadableWritablePair\":[],\"RecordingState\":[],\"RedEyeReduction\":[],\"ReferrerPolicy\":[],\"RegisterRequest\":[],\"RegisterResponse\":[],\"RegisteredKey\":[],\"RegistrationOptions\":[],\"RegistrationResponseJson\":[],\"Request\":[],\"RequestCache\":[],\"RequestCredentials\":[],\"RequestDestination\":[],\"RequestDeviceOptions\":[],\"RequestInit\":[],\"RequestMediaKeySystemAccessNotification\":[],\"RequestMode\":[],\"RequestRedirect\":[],\"ResidentKeyRequirement\":[],\"ResizeObserver\":[],\"ResizeObserverBoxOptions\":[],\"ResizeObserverEntry\":[],\"ResizeObserverOptions\":[],\"ResizeObserverSize\":[],\"ResizeQuality\":[],\"Response\":[],\"ResponseInit\":[],\"ResponseType\":[],\"RsaHashedImportParams\":[],\"RsaOaepParams\":[],\"RsaOtherPrimesInfo\":[],\"RsaPssParams\":[],\"RtcAnswerOptions\":[],\"RtcBundlePolicy\":[],\"RtcCertificate\":[],\"RtcCertificateExpiration\":[],\"RtcCodecStats\":[],\"RtcConfiguration\":[],\"RtcDataChannel\":[\"EventTarget\"],\"RtcDataChannelEvent\":[\"Event\"],\"RtcDataChannelEventInit\":[],\"RtcDataChannelInit\":[],\"RtcDataChannelState\":[],\"RtcDataChannelType\":[],\"RtcDegradationPreference\":[],\"RtcEncodedAudioFrame\":[],\"RtcEncodedAudioFrameMetadata\":[],\"RtcEncodedAudioFrameOptions\":[],\"RtcEncodedVideoFrame\":[],\"RtcEncodedVideoFrameMetadata\":[],\"RtcEncodedVideoFrameOptions\":[],\"RtcEncodedVideoFrameType\":[],\"RtcFecParameters\":[],\"RtcIceCandidate\":[],\"RtcIceCandidateInit\":[],\"RtcIceCandidatePairStats\":[],\"RtcIceCandidateStats\":[],\"RtcIceComponentStats\":[],\"RtcIceConnectionState\":[],\"RtcIceCredentialType\":[],\"RtcIceGatheringState\":[],\"RtcIceServer\":[],\"RtcIceTransportPolicy\":[],\"RtcIdentityAssertion\":[],\"RtcIdentityAssertionResult\":[],\"RtcIdentityProvider\":[],\"RtcIdentityProviderDetails\":[],\"RtcIdentityProviderOptions\":[],\"RtcIdentityProviderRegistrar\":[],\"RtcIdentityValidationResult\":[],\"RtcInboundRtpStreamStats\":[],\"RtcMediaStreamStats\":[],\"RtcMediaStreamTrackStats\":[],\"RtcOfferAnswerOptions\":[],\"RtcOfferOptions\":[],\"RtcOutboundRtpStreamStats\":[],\"RtcPeerConnection\":[\"EventTarget\"],\"RtcPeerConnectionIceErrorEvent\":[\"Event\"],\"RtcPeerConnectionIceEvent\":[\"Event\"],\"RtcPeerConnectionIceEventInit\":[],\"RtcPeerConnectionState\":[],\"RtcPriorityType\":[],\"RtcRtcpParameters\":[],\"RtcRtpCapabilities\":[],\"RtcRtpCodecCapability\":[],\"RtcRtpCodecParameters\":[],\"RtcRtpContributingSource\":[],\"RtcRtpEncodingParameters\":[],\"RtcRtpHeaderExtensionCapability\":[],\"RtcRtpHeaderExtensionParameters\":[],\"RtcRtpParameters\":[],\"RtcRtpReceiver\":[],\"RtcRtpScriptTransform\":[],\"RtcRtpScriptTransformer\":[\"EventTarget\"],\"RtcRtpSender\":[],\"RtcRtpSourceEntry\":[],\"RtcRtpSourceEntryType\":[],\"RtcRtpSynchronizationSource\":[],\"RtcRtpTransceiver\":[],\"RtcRtpTransceiverDirection\":[],\"RtcRtpTransceiverInit\":[],\"RtcRtxParameters\":[],\"RtcSdpType\":[],\"RtcSessionDescription\":[],\"RtcSessionDescriptionInit\":[],\"RtcSignalingState\":[],\"RtcStats\":[],\"RtcStatsIceCandidatePairState\":[],\"RtcStatsIceCandidateType\":[],\"RtcStatsReport\":[],\"RtcStatsReportInternal\":[],\"RtcStatsType\":[],\"RtcTrackEvent\":[\"Event\"],\"RtcTrackEventInit\":[],\"RtcTransformEvent\":[\"Event\"],\"RtcTransportStats\":[],\"RtcdtmfSender\":[\"EventTarget\"],\"RtcdtmfToneChangeEvent\":[\"Event\"],\"RtcdtmfToneChangeEventInit\":[],\"RtcrtpContributingSourceStats\":[],\"RtcrtpStreamStats\":[],\"SFrameTransform\":[\"EventTarget\"],\"SFrameTransformErrorEvent\":[\"Event\"],\"SFrameTransformErrorEventInit\":[],\"SFrameTransformErrorEventType\":[],\"SFrameTransformOptions\":[],\"SFrameTransformRole\":[],\"SaveFilePickerOptions\":[],\"Scheduler\":[],\"SchedulerPostTaskOptions\":[],\"Scheduling\":[],\"Screen\":[\"EventTarget\"],\"ScreenColorGamut\":[],\"ScreenDetailed\":[\"EventTarget\",\"Screen\"],\"ScreenDetails\":[\"EventTarget\"],\"ScreenLuminance\":[],\"ScreenOrientation\":[\"EventTarget\"],\"ScriptProcessorNode\":[\"AudioNode\",\"EventTarget\"],\"ScrollAreaEvent\":[\"Event\",\"UiEvent\"],\"ScrollBehavior\":[],\"ScrollBoxObject\":[],\"ScrollIntoViewContainer\":[],\"ScrollIntoViewOptions\":[],\"ScrollLogicalPosition\":[],\"ScrollOptions\":[],\"ScrollRestoration\":[],\"ScrollSetting\":[],\"ScrollState\":[],\"ScrollToOptions\":[],\"ScrollViewChangeEventInit\":[],\"SecurityPolicyViolationEvent\":[\"Event\"],\"SecurityPolicyViolationEventDisposition\":[],\"SecurityPolicyViolationEventInit\":[],\"Selection\":[],\"SelectionMode\":[],\"Serial\":[\"EventTarget\"],\"SerialInputSignals\":[],\"SerialOptions\":[],\"SerialOutputSignals\":[],\"SerialPort\":[\"EventTarget\"],\"SerialPortFilter\":[],\"SerialPortInfo\":[],\"SerialPortRequestOptions\":[],\"ServerSocketOptions\":[],\"ServiceWorker\":[\"EventTarget\"],\"ServiceWorkerContainer\":[\"EventTarget\"],\"ServiceWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"ServiceWorkerRegistration\":[\"EventTarget\"],\"ServiceWorkerState\":[],\"ServiceWorkerUpdateViaCache\":[],\"ShadowRoot\":[\"DocumentFragment\",\"EventTarget\",\"Node\"],\"ShadowRootInit\":[],\"ShadowRootMode\":[],\"ShareData\":[],\"SharedWorker\":[\"EventTarget\"],\"SharedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"ShowPopoverOptions\":[],\"SignResponse\":[],\"SocketElement\":[],\"SocketOptions\":[],\"SocketReadyState\":[],\"SocketsDict\":[],\"SourceBuffer\":[\"EventTarget\"],\"SourceBufferAppendMode\":[],\"SourceBufferList\":[\"EventTarget\"],\"SpeechGrammar\":[],\"SpeechGrammarList\":[],\"SpeechRecognition\":[\"EventTarget\"],\"SpeechRecognitionAlternative\":[],\"SpeechRecognitionError\":[\"Event\"],\"SpeechRecognitionErrorCode\":[],\"SpeechRecognitionErrorInit\":[],\"SpeechRecognitionEvent\":[\"Event\"],\"SpeechRecognitionEventInit\":[],\"SpeechRecognitionResult\":[],\"SpeechRecognitionResultList\":[],\"SpeechSynthesis\":[\"EventTarget\"],\"SpeechSynthesisErrorCode\":[],\"SpeechSynthesisErrorEvent\":[\"Event\",\"SpeechSynthesisEvent\"],\"SpeechSynthesisErrorEventInit\":[],\"SpeechSynthesisEvent\":[\"Event\"],\"SpeechSynthesisEventInit\":[],\"SpeechSynthesisUtterance\":[\"EventTarget\"],\"SpeechSynthesisVoice\":[],\"StaticRange\":[\"AbstractRange\"],\"StaticRangeInit\":[],\"StereoPannerNode\":[\"AudioNode\",\"EventTarget\"],\"StereoPannerOptions\":[],\"Storage\":[],\"StorageEstimate\":[],\"StorageEvent\":[\"Event\"],\"StorageEventInit\":[],\"StorageManager\":[],\"StorageType\":[],\"StreamPipeOptions\":[],\"StyleRuleChangeEventInit\":[],\"StyleSheet\":[],\"StyleSheetApplicableStateChangeEventInit\":[],\"StyleSheetChangeEventInit\":[],\"StyleSheetList\":[],\"SubmitEvent\":[\"Event\"],\"SubmitEventInit\":[],\"SubtleCrypto\":[],\"SupportedType\":[],\"SvcOutputMetadata\":[],\"SvgAngle\":[],\"SvgAnimateElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateMotionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateTransformElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimatedAngle\":[],\"SvgAnimatedBoolean\":[],\"SvgAnimatedEnumeration\":[],\"SvgAnimatedInteger\":[],\"SvgAnimatedLength\":[],\"SvgAnimatedLengthList\":[],\"SvgAnimatedNumber\":[],\"SvgAnimatedNumberList\":[],\"SvgAnimatedPreserveAspectRatio\":[],\"SvgAnimatedRect\":[],\"SvgAnimatedString\":[],\"SvgAnimatedTransformList\":[],\"SvgAnimationElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgBoundingBoxOptions\":[],\"SvgCircleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgClipPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgComponentTransferFunctionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgDefsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgDescElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgElement\":[\"Element\",\"EventTarget\",\"Node\"],\"SvgEllipseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgFilterElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgForeignObjectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGeometryElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgGraphicsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgLength\":[],\"SvgLengthList\":[],\"SvgLineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgLinearGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgMarkerElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMaskElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMatrix\":[],\"SvgMetadataElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgNumber\":[],\"SvgNumberList\":[],\"SvgPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPathSeg\":[],\"SvgPathSegArcAbs\":[\"SvgPathSeg\"],\"SvgPathSegArcRel\":[\"SvgPathSeg\"],\"SvgPathSegClosePath\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalRel\":[\"SvgPathSeg\"],\"SvgPathSegList\":[],\"SvgPathSegMovetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegMovetoRel\":[\"SvgPathSeg\"],\"SvgPatternElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgPoint\":[],\"SvgPointList\":[],\"SvgPolygonElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPolylineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPreserveAspectRatio\":[],\"SvgRadialGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgRect\":[],\"SvgRectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgScriptElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgStopElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgStringList\":[],\"SvgStyleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSwitchElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgSymbolElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTextContentElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgTextElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"SvgTextPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTextPositioningElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTitleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTransform\":[],\"SvgTransformList\":[],\"SvgUnitTypes\":[],\"SvgUseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgViewElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgZoomAndPan\":[],\"SvgaElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgfeBlendElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeColorMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeComponentTransferElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeCompositeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeConvolveMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDiffuseLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDisplacementMapElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDistantLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDropShadowElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFloodElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFuncAElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncBElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncGElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncRElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeGaussianBlurElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeNodeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMorphologyElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeOffsetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfePointLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpecularLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpotLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTileElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTurbulenceElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvggElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgmPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgsvgElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgtSpanElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"TaskController\":[\"AbortController\"],\"TaskControllerInit\":[],\"TaskPriority\":[],\"TaskPriorityChangeEvent\":[\"Event\"],\"TaskPriorityChangeEventInit\":[],\"TaskSignal\":[\"AbortSignal\",\"EventTarget\"],\"TaskSignalAnyInit\":[],\"TcpReadyState\":[],\"TcpServerSocket\":[\"EventTarget\"],\"TcpServerSocketEvent\":[\"Event\"],\"TcpServerSocketEventInit\":[],\"TcpSocket\":[\"EventTarget\"],\"TcpSocketBinaryType\":[],\"TcpSocketErrorEvent\":[\"Event\"],\"TcpSocketErrorEventInit\":[],\"TcpSocketEvent\":[\"Event\"],\"TcpSocketEventInit\":[],\"Text\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"TextDecodeOptions\":[],\"TextDecoder\":[],\"TextDecoderOptions\":[],\"TextEncoder\":[],\"TextMetrics\":[],\"TextTrack\":[\"EventTarget\"],\"TextTrackCue\":[\"EventTarget\"],\"TextTrackCueList\":[],\"TextTrackKind\":[],\"TextTrackList\":[\"EventTarget\"],\"TextTrackMode\":[],\"TimeEvent\":[\"Event\"],\"TimeRanges\":[],\"ToggleEvent\":[\"Event\"],\"ToggleEventInit\":[],\"TogglePopoverOptions\":[],\"TokenBinding\":[],\"TokenBindingStatus\":[],\"Touch\":[],\"TouchEvent\":[\"Event\",\"UiEvent\"],\"TouchEventInit\":[],\"TouchInit\":[],\"TouchList\":[],\"TrackEvent\":[\"Event\"],\"TrackEventInit\":[],\"TransformStream\":[],\"TransformStreamDefaultController\":[],\"Transformer\":[],\"TransitionEvent\":[\"Event\"],\"TransitionEventInit\":[],\"Transport\":[],\"TreeBoxObject\":[],\"TreeCellInfo\":[],\"TreeView\":[],\"TreeWalker\":[],\"U2f\":[],\"U2fClientData\":[],\"ULongRange\":[],\"UaDataValues\":[],\"UaLowEntropyJson\":[],\"UdpMessageEventInit\":[],\"UdpOptions\":[],\"UiEvent\":[\"Event\"],\"UiEventInit\":[],\"UnderlyingSink\":[],\"UnderlyingSource\":[],\"Url\":[],\"UrlSearchParams\":[],\"Usb\":[\"EventTarget\"],\"UsbAlternateInterface\":[],\"UsbConfiguration\":[],\"UsbConnectionEvent\":[\"Event\"],\"UsbConnectionEventInit\":[],\"UsbControlTransferParameters\":[],\"UsbDevice\":[],\"UsbDeviceFilter\":[],\"UsbDeviceRequestOptions\":[],\"UsbDirection\":[],\"UsbEndpoint\":[],\"UsbEndpointType\":[],\"UsbInTransferResult\":[],\"UsbInterface\":[],\"UsbIsochronousInTransferPacket\":[],\"UsbIsochronousInTransferResult\":[],\"UsbIsochronousOutTransferPacket\":[],\"UsbIsochronousOutTransferResult\":[],\"UsbOutTransferResult\":[],\"UsbPermissionDescriptor\":[],\"UsbPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"UsbPermissionStorage\":[],\"UsbRecipient\":[],\"UsbRequestType\":[],\"UsbTransferStatus\":[],\"UserActivation\":[],\"UserProximityEvent\":[\"Event\"],\"UserProximityEventInit\":[],\"UserVerificationRequirement\":[],\"ValidityState\":[],\"ValueEvent\":[\"Event\"],\"ValueEventInit\":[],\"VideoColorPrimaries\":[],\"VideoColorSpace\":[],\"VideoColorSpaceInit\":[],\"VideoConfiguration\":[],\"VideoDecoder\":[\"EventTarget\"],\"VideoDecoderConfig\":[],\"VideoDecoderInit\":[],\"VideoDecoderSupport\":[],\"VideoEncoder\":[\"EventTarget\"],\"VideoEncoderBitrateMode\":[],\"VideoEncoderConfig\":[],\"VideoEncoderEncodeOptions\":[],\"VideoEncoderInit\":[],\"VideoEncoderSupport\":[],\"VideoFacingModeEnum\":[],\"VideoFrame\":[],\"VideoFrameBufferInit\":[],\"VideoFrameCopyToOptions\":[],\"VideoFrameInit\":[],\"VideoFrameMetadata\":[],\"VideoMatrixCoefficients\":[],\"VideoPixelFormat\":[],\"VideoPlaybackQuality\":[],\"VideoStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"VideoTrack\":[],\"VideoTrackList\":[\"EventTarget\"],\"VideoTransferCharacteristics\":[],\"ViewTransition\":[],\"VisibilityState\":[],\"VisualViewport\":[\"EventTarget\"],\"VoidCallback\":[],\"VrDisplay\":[\"EventTarget\"],\"VrDisplayCapabilities\":[],\"VrEye\":[],\"VrEyeParameters\":[],\"VrFieldOfView\":[],\"VrFrameData\":[],\"VrLayer\":[],\"VrMockController\":[],\"VrMockDisplay\":[],\"VrPose\":[],\"VrServiceTest\":[],\"VrStageParameters\":[],\"VrSubmitFrameResult\":[],\"VttCue\":[\"EventTarget\",\"TextTrackCue\"],\"VttRegion\":[],\"WakeLock\":[],\"WakeLockSentinel\":[\"EventTarget\"],\"WakeLockType\":[],\"WatchAdvertisementsOptions\":[],\"WaveShaperNode\":[\"AudioNode\",\"EventTarget\"],\"WaveShaperOptions\":[],\"WebGl2RenderingContext\":[],\"WebGlActiveInfo\":[],\"WebGlBuffer\":[],\"WebGlContextAttributes\":[],\"WebGlContextEvent\":[\"Event\"],\"WebGlContextEventInit\":[],\"WebGlFramebuffer\":[],\"WebGlPowerPreference\":[],\"WebGlProgram\":[],\"WebGlQuery\":[],\"WebGlRenderbuffer\":[],\"WebGlRenderingContext\":[],\"WebGlSampler\":[],\"WebGlShader\":[],\"WebGlShaderPrecisionFormat\":[],\"WebGlSync\":[],\"WebGlTexture\":[],\"WebGlTransformFeedback\":[],\"WebGlUniformLocation\":[],\"WebGlVertexArrayObject\":[],\"WebKitCssMatrix\":[\"DomMatrix\",\"DomMatrixReadOnly\"],\"WebSocket\":[\"EventTarget\"],\"WebSocketDict\":[],\"WebSocketElement\":[],\"WebTransport\":[],\"WebTransportBidirectionalStream\":[],\"WebTransportCloseInfo\":[],\"WebTransportCongestionControl\":[],\"WebTransportDatagramDuplexStream\":[],\"WebTransportDatagramStats\":[],\"WebTransportError\":[\"DomException\"],\"WebTransportErrorOptions\":[],\"WebTransportErrorSource\":[],\"WebTransportHash\":[],\"WebTransportOptions\":[],\"WebTransportReceiveStream\":[\"ReadableStream\"],\"WebTransportReceiveStreamStats\":[],\"WebTransportReliabilityMode\":[],\"WebTransportSendStream\":[\"WritableStream\"],\"WebTransportSendStreamOptions\":[],\"WebTransportSendStreamStats\":[],\"WebTransportStats\":[],\"WebglColorBufferFloat\":[],\"WebglCompressedTextureAstc\":[],\"WebglCompressedTextureAtc\":[],\"WebglCompressedTextureEtc\":[],\"WebglCompressedTextureEtc1\":[],\"WebglCompressedTexturePvrtc\":[],\"WebglCompressedTextureS3tc\":[],\"WebglCompressedTextureS3tcSrgb\":[],\"WebglDebugRendererInfo\":[],\"WebglDebugShaders\":[],\"WebglDepthTexture\":[],\"WebglDrawBuffers\":[],\"WebglLoseContext\":[],\"WebglMultiDraw\":[],\"WellKnownDirectory\":[],\"WgslLanguageFeatures\":[],\"WheelEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"WheelEventInit\":[],\"WidevineCdmManifest\":[],\"Window\":[\"EventTarget\"],\"WindowClient\":[\"Client\"],\"Worker\":[\"EventTarget\"],\"WorkerDebuggerGlobalScope\":[\"EventTarget\"],\"WorkerGlobalScope\":[\"EventTarget\"],\"WorkerLocation\":[],\"WorkerNavigator\":[],\"WorkerOptions\":[],\"WorkerType\":[],\"Worklet\":[],\"WorkletGlobalScope\":[],\"WorkletOptions\":[],\"WritableStream\":[],\"WritableStreamDefaultController\":[],\"WritableStreamDefaultWriter\":[],\"WriteCommandType\":[],\"WriteParams\":[],\"XPathExpression\":[],\"XPathNsResolver\":[],\"XPathResult\":[],\"XmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"XmlHttpRequest\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlHttpRequestEventTarget\":[\"EventTarget\"],\"XmlHttpRequestResponseType\":[],\"XmlHttpRequestUpload\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlSerializer\":[],\"XrBoundedReferenceSpace\":[\"EventTarget\",\"XrReferenceSpace\",\"XrSpace\"],\"XrEye\":[],\"XrFrame\":[],\"XrHand\":[],\"XrHandJoint\":[],\"XrHandedness\":[],\"XrInputSource\":[],\"XrInputSourceArray\":[],\"XrInputSourceEvent\":[\"Event\"],\"XrInputSourceEventInit\":[],\"XrInputSourcesChangeEvent\":[\"Event\"],\"XrInputSourcesChangeEventInit\":[],\"XrJointPose\":[\"XrPose\"],\"XrJointSpace\":[\"EventTarget\",\"XrSpace\"],\"XrLayer\":[\"EventTarget\"],\"XrPermissionDescriptor\":[],\"XrPermissionStatus\":[\"EventTarget\",\"PermissionStatus\"],\"XrPose\":[],\"XrReferenceSpace\":[\"EventTarget\",\"XrSpace\"],\"XrReferenceSpaceEvent\":[\"Event\"],\"XrReferenceSpaceEventInit\":[],\"XrReferenceSpaceType\":[],\"XrRenderState\":[],\"XrRenderStateInit\":[],\"XrRigidTransform\":[],\"XrSession\":[\"EventTarget\"],\"XrSessionEvent\":[\"Event\"],\"XrSessionEventInit\":[],\"XrSessionInit\":[],\"XrSessionMode\":[],\"XrSessionSupportedPermissionDescriptor\":[],\"XrSpace\":[\"EventTarget\"],\"XrSystem\":[\"EventTarget\"],\"XrTargetRayMode\":[],\"XrView\":[],\"XrViewerPose\":[\"XrPose\"],\"XrViewport\":[],\"XrVisibilityState\":[],\"XrWebGlLayer\":[\"EventTarget\",\"XrLayer\"],\"XrWebGlLayerInit\":[],\"XsltProcessor\":[],\"console\":[],\"css\":[],\"default\":[\"std\"],\"gpu_buffer_usage\":[],\"gpu_color_write\":[],\"gpu_map_mode\":[],\"gpu_shader_stage\":[],\"gpu_texture_usage\":[],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\"]}}", + "web-sys_0.3.85": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.85\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"AbortController\":[],\"AbortSignal\":[\"EventTarget\"],\"AddEventListenerOptions\":[],\"AesCbcParams\":[],\"AesCtrParams\":[],\"AesDerivedKeyParams\":[],\"AesGcmParams\":[],\"AesKeyAlgorithm\":[],\"AesKeyGenParams\":[],\"Algorithm\":[],\"AlignSetting\":[],\"AllowedBluetoothDevice\":[],\"AllowedUsbDevice\":[],\"AlphaOption\":[],\"AnalyserNode\":[\"AudioNode\",\"EventTarget\"],\"AnalyserOptions\":[],\"AngleInstancedArrays\":[],\"Animation\":[\"EventTarget\"],\"AnimationEffect\":[],\"AnimationEvent\":[\"Event\"],\"AnimationEventInit\":[],\"AnimationPlayState\":[],\"AnimationPlaybackEvent\":[\"Event\"],\"AnimationPlaybackEventInit\":[],\"AnimationPropertyDetails\":[],\"AnimationPropertyValueDetails\":[],\"AnimationTimeline\":[],\"AssignedNodesOptions\":[],\"AttestationConveyancePreference\":[],\"Attr\":[\"EventTarget\",\"Node\"],\"AttributeNameValue\":[],\"AudioBuffer\":[],\"AudioBufferOptions\":[],\"AudioBufferSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"AudioBufferSourceOptions\":[],\"AudioConfiguration\":[],\"AudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"AudioContextLatencyCategory\":[],\"AudioContextOptions\":[],\"AudioContextState\":[],\"AudioData\":[],\"AudioDataCopyToOptions\":[],\"AudioDataInit\":[],\"AudioDecoder\":[],\"AudioDecoderConfig\":[],\"AudioDecoderInit\":[],\"AudioDecoderSupport\":[],\"AudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"AudioEncoder\":[],\"AudioEncoderConfig\":[],\"AudioEncoderInit\":[],\"AudioEncoderSupport\":[],\"AudioListener\":[],\"AudioNode\":[\"EventTarget\"],\"AudioNodeOptions\":[],\"AudioParam\":[],\"AudioParamMap\":[],\"AudioProcessingEvent\":[\"Event\"],\"AudioSampleFormat\":[],\"AudioScheduledSourceNode\":[\"AudioNode\",\"EventTarget\"],\"AudioSinkInfo\":[],\"AudioSinkOptions\":[],\"AudioSinkType\":[],\"AudioStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"AudioTrack\":[],\"AudioTrackList\":[\"EventTarget\"],\"AudioWorklet\":[\"Worklet\"],\"AudioWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"AudioWorkletNode\":[\"AudioNode\",\"EventTarget\"],\"AudioWorkletNodeOptions\":[],\"AudioWorkletProcessor\":[],\"AuthenticationExtensionsClientInputs\":[],\"AuthenticationExtensionsClientInputsJson\":[],\"AuthenticationExtensionsClientOutputs\":[],\"AuthenticationExtensionsClientOutputsJson\":[],\"AuthenticationExtensionsDevicePublicKeyInputs\":[],\"AuthenticationExtensionsDevicePublicKeyOutputs\":[],\"AuthenticationExtensionsLargeBlobInputs\":[],\"AuthenticationExtensionsLargeBlobOutputs\":[],\"AuthenticationExtensionsPrfInputs\":[],\"AuthenticationExtensionsPrfOutputs\":[],\"AuthenticationExtensionsPrfValues\":[],\"AuthenticationResponseJson\":[],\"AuthenticatorAssertionResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAssertionResponseJson\":[],\"AuthenticatorAttachment\":[],\"AuthenticatorAttestationResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAttestationResponseJson\":[],\"AuthenticatorResponse\":[],\"AuthenticatorSelectionCriteria\":[],\"AuthenticatorTransport\":[],\"AutoKeyword\":[],\"AutocompleteInfo\":[],\"BarProp\":[],\"BaseAudioContext\":[\"EventTarget\"],\"BaseComputedKeyframe\":[],\"BaseKeyframe\":[],\"BasePropertyIndexedKeyframe\":[],\"BasicCardRequest\":[],\"BasicCardResponse\":[],\"BasicCardType\":[],\"BatteryManager\":[\"EventTarget\"],\"BeforeUnloadEvent\":[\"Event\"],\"BinaryType\":[],\"BiquadFilterNode\":[\"AudioNode\",\"EventTarget\"],\"BiquadFilterOptions\":[],\"BiquadFilterType\":[],\"Blob\":[],\"BlobEvent\":[\"Event\"],\"BlobEventInit\":[],\"BlobPropertyBag\":[],\"BlockParsingOptions\":[],\"Bluetooth\":[\"EventTarget\"],\"BluetoothAdvertisingEvent\":[\"Event\"],\"BluetoothAdvertisingEventInit\":[],\"BluetoothCharacteristicProperties\":[],\"BluetoothDataFilterInit\":[],\"BluetoothDevice\":[\"EventTarget\"],\"BluetoothLeScanFilterInit\":[],\"BluetoothManufacturerDataMap\":[],\"BluetoothPermissionDescriptor\":[],\"BluetoothPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"BluetoothPermissionStorage\":[],\"BluetoothRemoteGattCharacteristic\":[\"EventTarget\"],\"BluetoothRemoteGattDescriptor\":[],\"BluetoothRemoteGattServer\":[],\"BluetoothRemoteGattService\":[\"EventTarget\"],\"BluetoothServiceDataMap\":[],\"BluetoothUuid\":[],\"BoxQuadOptions\":[],\"BroadcastChannel\":[\"EventTarget\"],\"BrowserElementDownloadOptions\":[],\"BrowserElementExecuteScriptOptions\":[],\"BrowserFeedWriter\":[],\"BrowserFindCaseSensitivity\":[],\"BrowserFindDirection\":[],\"ByteLengthQueuingStrategy\":[],\"Cache\":[],\"CacheBatchOperation\":[],\"CacheQueryOptions\":[],\"CacheStorage\":[],\"CacheStorageNamespace\":[],\"CanvasCaptureMediaStream\":[\"EventTarget\",\"MediaStream\"],\"CanvasCaptureMediaStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"CanvasGradient\":[],\"CanvasPattern\":[],\"CanvasRenderingContext2d\":[],\"CanvasWindingRule\":[],\"CaretChangedReason\":[],\"CaretPosition\":[],\"CaretStateChangedEventInit\":[],\"CdataSection\":[\"CharacterData\",\"EventTarget\",\"Node\",\"Text\"],\"ChannelCountMode\":[],\"ChannelInterpretation\":[],\"ChannelMergerNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelMergerOptions\":[],\"ChannelSplitterNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelSplitterOptions\":[],\"CharacterData\":[\"EventTarget\",\"Node\"],\"CheckerboardReason\":[],\"CheckerboardReport\":[],\"CheckerboardReportService\":[],\"ChromeFilePropertyBag\":[],\"ChromeWorker\":[\"EventTarget\",\"Worker\"],\"Client\":[],\"ClientQueryOptions\":[],\"ClientRectsAndTexts\":[],\"ClientType\":[],\"Clients\":[],\"Clipboard\":[\"EventTarget\"],\"ClipboardEvent\":[\"Event\"],\"ClipboardEventInit\":[],\"ClipboardItem\":[],\"ClipboardItemOptions\":[],\"ClipboardPermissionDescriptor\":[],\"ClipboardUnsanitizedFormats\":[],\"CloseEvent\":[\"Event\"],\"CloseEventInit\":[],\"CodecState\":[],\"CollectedClientData\":[],\"ColorSpaceConversion\":[],\"Comment\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"CompositeOperation\":[],\"CompositionEvent\":[\"Event\",\"UiEvent\"],\"CompositionEventInit\":[],\"CompressionFormat\":[],\"CompressionStream\":[],\"ComputedEffectTiming\":[],\"ConnStatusDict\":[],\"ConnectionType\":[],\"ConsoleCounter\":[],\"ConsoleCounterError\":[],\"ConsoleEvent\":[],\"ConsoleInstance\":[],\"ConsoleInstanceOptions\":[],\"ConsoleLevel\":[],\"ConsoleLogLevel\":[],\"ConsoleProfileEvent\":[],\"ConsoleStackEntry\":[],\"ConsoleTimerError\":[],\"ConsoleTimerLogOrEnd\":[],\"ConsoleTimerStart\":[],\"ConstantSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"ConstantSourceOptions\":[],\"ConstrainBooleanParameters\":[],\"ConstrainDomStringParameters\":[],\"ConstrainDoubleRange\":[],\"ConstrainLongRange\":[],\"ContextAttributes2d\":[],\"ConvertCoordinateOptions\":[],\"ConvolverNode\":[\"AudioNode\",\"EventTarget\"],\"ConvolverOptions\":[],\"CookieChangeEvent\":[\"Event\"],\"CookieChangeEventInit\":[],\"CookieInit\":[],\"CookieListItem\":[],\"CookieSameSite\":[],\"CookieStore\":[\"EventTarget\"],\"CookieStoreDeleteOptions\":[],\"CookieStoreGetOptions\":[],\"CookieStoreManager\":[],\"Coordinates\":[],\"CountQueuingStrategy\":[],\"Credential\":[],\"CredentialCreationOptions\":[],\"CredentialPropertiesOutput\":[],\"CredentialRequestOptions\":[],\"CredentialsContainer\":[],\"Crypto\":[],\"CryptoKey\":[],\"CryptoKeyPair\":[],\"CssAnimation\":[\"Animation\",\"EventTarget\"],\"CssBoxType\":[],\"CssConditionRule\":[\"CssGroupingRule\",\"CssRule\"],\"CssCounterStyleRule\":[\"CssRule\"],\"CssFontFaceRule\":[\"CssRule\"],\"CssFontFeatureValuesRule\":[\"CssRule\"],\"CssGroupingRule\":[\"CssRule\"],\"CssImportRule\":[\"CssRule\"],\"CssKeyframeRule\":[\"CssRule\"],\"CssKeyframesRule\":[\"CssRule\"],\"CssMediaRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssNamespaceRule\":[\"CssRule\"],\"CssPageRule\":[\"CssRule\"],\"CssPseudoElement\":[],\"CssRule\":[],\"CssRuleList\":[],\"CssStyleDeclaration\":[],\"CssStyleRule\":[\"CssRule\"],\"CssStyleSheet\":[\"StyleSheet\"],\"CssStyleSheetParsingMode\":[],\"CssSupportsRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssTransition\":[\"Animation\",\"EventTarget\"],\"CustomElementRegistry\":[],\"CustomEvent\":[\"Event\"],\"CustomEventInit\":[],\"DataTransfer\":[],\"DataTransferItem\":[],\"DataTransferItemList\":[],\"DateTimeValue\":[],\"DecoderDoctorNotification\":[],\"DecoderDoctorNotificationType\":[],\"DecompressionStream\":[],\"DedicatedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"DelayNode\":[\"AudioNode\",\"EventTarget\"],\"DelayOptions\":[],\"DeviceAcceleration\":[],\"DeviceAccelerationInit\":[],\"DeviceLightEvent\":[\"Event\"],\"DeviceLightEventInit\":[],\"DeviceMotionEvent\":[\"Event\"],\"DeviceMotionEventInit\":[],\"DeviceOrientationEvent\":[\"Event\"],\"DeviceOrientationEventInit\":[],\"DeviceProximityEvent\":[\"Event\"],\"DeviceProximityEventInit\":[],\"DeviceRotationRate\":[],\"DeviceRotationRateInit\":[],\"DhKeyDeriveParams\":[],\"DirectionSetting\":[],\"Directory\":[],\"DirectoryPickerOptions\":[],\"DisplayMediaStreamConstraints\":[],\"DisplayNameOptions\":[],\"DisplayNameResult\":[],\"DistanceModelType\":[],\"DnsCacheDict\":[],\"DnsCacheEntry\":[],\"DnsLookupDict\":[],\"Document\":[\"EventTarget\",\"Node\"],\"DocumentFragment\":[\"EventTarget\",\"Node\"],\"DocumentTimeline\":[\"AnimationTimeline\"],\"DocumentTimelineOptions\":[],\"DocumentType\":[\"EventTarget\",\"Node\"],\"DomError\":[],\"DomException\":[],\"DomImplementation\":[],\"DomMatrix\":[\"DomMatrixReadOnly\"],\"DomMatrix2dInit\":[],\"DomMatrixInit\":[],\"DomMatrixReadOnly\":[],\"DomParser\":[],\"DomPoint\":[\"DomPointReadOnly\"],\"DomPointInit\":[],\"DomPointReadOnly\":[],\"DomQuad\":[],\"DomQuadInit\":[],\"DomQuadJson\":[],\"DomRect\":[\"DomRectReadOnly\"],\"DomRectInit\":[],\"DomRectList\":[],\"DomRectReadOnly\":[],\"DomRequest\":[\"EventTarget\"],\"DomRequestReadyState\":[],\"DomStringList\":[],\"DomStringMap\":[],\"DomTokenList\":[],\"DomWindowResizeEventDetail\":[],\"DoubleRange\":[],\"DragEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"DragEventInit\":[],\"DynamicsCompressorNode\":[\"AudioNode\",\"EventTarget\"],\"DynamicsCompressorOptions\":[],\"EcKeyAlgorithm\":[],\"EcKeyGenParams\":[],\"EcKeyImportParams\":[],\"EcdhKeyDeriveParams\":[],\"EcdsaParams\":[],\"EffectTiming\":[],\"Element\":[\"EventTarget\",\"Node\"],\"ElementCreationOptions\":[],\"ElementDefinitionOptions\":[],\"EncodedAudioChunk\":[],\"EncodedAudioChunkInit\":[],\"EncodedAudioChunkMetadata\":[],\"EncodedAudioChunkType\":[],\"EncodedVideoChunk\":[],\"EncodedVideoChunkInit\":[],\"EncodedVideoChunkMetadata\":[],\"EncodedVideoChunkType\":[],\"EndingTypes\":[],\"ErrorCallback\":[],\"ErrorEvent\":[\"Event\"],\"ErrorEventInit\":[],\"Event\":[],\"EventInit\":[],\"EventListener\":[],\"EventListenerOptions\":[],\"EventModifierInit\":[],\"EventSource\":[\"EventTarget\"],\"EventSourceInit\":[],\"EventTarget\":[],\"Exception\":[],\"ExtBlendMinmax\":[],\"ExtColorBufferFloat\":[],\"ExtColorBufferHalfFloat\":[],\"ExtDisjointTimerQuery\":[],\"ExtFragDepth\":[],\"ExtSRgb\":[],\"ExtShaderTextureLod\":[],\"ExtTextureFilterAnisotropic\":[],\"ExtTextureNorm16\":[],\"ExtendableCookieChangeEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableCookieChangeEventInit\":[],\"ExtendableEvent\":[\"Event\"],\"ExtendableEventInit\":[],\"ExtendableMessageEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableMessageEventInit\":[],\"External\":[],\"FakePluginMimeEntry\":[],\"FakePluginTagInit\":[],\"FetchEvent\":[\"Event\",\"ExtendableEvent\"],\"FetchEventInit\":[],\"FetchObserver\":[\"EventTarget\"],\"FetchReadableStreamReadDataArray\":[],\"FetchReadableStreamReadDataDone\":[],\"FetchState\":[],\"File\":[\"Blob\"],\"FileCallback\":[],\"FileList\":[],\"FilePickerAcceptType\":[],\"FilePickerOptions\":[],\"FilePropertyBag\":[],\"FileReader\":[\"EventTarget\"],\"FileReaderSync\":[],\"FileSystem\":[],\"FileSystemCreateWritableOptions\":[],\"FileSystemDirectoryEntry\":[\"FileSystemEntry\"],\"FileSystemDirectoryHandle\":[\"FileSystemHandle\"],\"FileSystemDirectoryReader\":[],\"FileSystemEntriesCallback\":[],\"FileSystemEntry\":[],\"FileSystemEntryCallback\":[],\"FileSystemFileEntry\":[\"FileSystemEntry\"],\"FileSystemFileHandle\":[\"FileSystemHandle\"],\"FileSystemFlags\":[],\"FileSystemGetDirectoryOptions\":[],\"FileSystemGetFileOptions\":[],\"FileSystemHandle\":[],\"FileSystemHandleKind\":[],\"FileSystemHandlePermissionDescriptor\":[],\"FileSystemPermissionDescriptor\":[],\"FileSystemPermissionMode\":[],\"FileSystemReadWriteOptions\":[],\"FileSystemRemoveOptions\":[],\"FileSystemSyncAccessHandle\":[],\"FileSystemWritableFileStream\":[\"WritableStream\"],\"FillMode\":[],\"FlashClassification\":[],\"FlowControlType\":[],\"FocusEvent\":[\"Event\",\"UiEvent\"],\"FocusEventInit\":[],\"FocusOptions\":[],\"FontData\":[],\"FontFace\":[],\"FontFaceDescriptors\":[],\"FontFaceLoadStatus\":[],\"FontFaceSet\":[\"EventTarget\"],\"FontFaceSetIterator\":[],\"FontFaceSetIteratorResult\":[],\"FontFaceSetLoadEvent\":[\"Event\"],\"FontFaceSetLoadEventInit\":[],\"FontFaceSetLoadStatus\":[],\"FormData\":[],\"FrameType\":[],\"FuzzingFunctions\":[],\"GainNode\":[\"AudioNode\",\"EventTarget\"],\"GainOptions\":[],\"Gamepad\":[],\"GamepadButton\":[],\"GamepadEffectParameters\":[],\"GamepadEvent\":[\"Event\"],\"GamepadEventInit\":[],\"GamepadHand\":[],\"GamepadHapticActuator\":[],\"GamepadHapticActuatorType\":[],\"GamepadHapticEffectType\":[],\"GamepadHapticsResult\":[],\"GamepadMappingType\":[],\"GamepadPose\":[],\"GamepadTouch\":[],\"Geolocation\":[],\"GestureEvent\":[\"Event\",\"UiEvent\"],\"GetAnimationsOptions\":[],\"GetRootNodeOptions\":[],\"GetUserMediaRequest\":[],\"Gpu\":[],\"GpuAdapter\":[],\"GpuAdapterInfo\":[],\"GpuAddressMode\":[],\"GpuAutoLayoutMode\":[],\"GpuBindGroup\":[],\"GpuBindGroupDescriptor\":[],\"GpuBindGroupEntry\":[],\"GpuBindGroupLayout\":[],\"GpuBindGroupLayoutDescriptor\":[],\"GpuBindGroupLayoutEntry\":[],\"GpuBlendComponent\":[],\"GpuBlendFactor\":[],\"GpuBlendOperation\":[],\"GpuBlendState\":[],\"GpuBuffer\":[],\"GpuBufferBinding\":[],\"GpuBufferBindingLayout\":[],\"GpuBufferBindingType\":[],\"GpuBufferDescriptor\":[],\"GpuBufferMapState\":[],\"GpuCanvasAlphaMode\":[],\"GpuCanvasConfiguration\":[],\"GpuCanvasContext\":[],\"GpuCanvasToneMapping\":[],\"GpuCanvasToneMappingMode\":[],\"GpuColorDict\":[],\"GpuColorTargetState\":[],\"GpuCommandBuffer\":[],\"GpuCommandBufferDescriptor\":[],\"GpuCommandEncoder\":[],\"GpuCommandEncoderDescriptor\":[],\"GpuCompareFunction\":[],\"GpuCompilationInfo\":[],\"GpuCompilationMessage\":[],\"GpuCompilationMessageType\":[],\"GpuComputePassDescriptor\":[],\"GpuComputePassEncoder\":[],\"GpuComputePassTimestampWrites\":[],\"GpuComputePipeline\":[],\"GpuComputePipelineDescriptor\":[],\"GpuCopyExternalImageDestInfo\":[],\"GpuCopyExternalImageSourceInfo\":[],\"GpuCullMode\":[],\"GpuDepthStencilState\":[],\"GpuDevice\":[\"EventTarget\"],\"GpuDeviceDescriptor\":[],\"GpuDeviceLostInfo\":[],\"GpuDeviceLostReason\":[],\"GpuError\":[],\"GpuErrorFilter\":[],\"GpuExtent3dDict\":[],\"GpuExternalTexture\":[],\"GpuExternalTextureBindingLayout\":[],\"GpuExternalTextureDescriptor\":[],\"GpuFeatureName\":[],\"GpuFilterMode\":[],\"GpuFragmentState\":[],\"GpuFrontFace\":[],\"GpuIndexFormat\":[],\"GpuInternalError\":[\"GpuError\"],\"GpuLoadOp\":[],\"GpuMipmapFilterMode\":[],\"GpuMultisampleState\":[],\"GpuObjectDescriptorBase\":[],\"GpuOrigin2dDict\":[],\"GpuOrigin3dDict\":[],\"GpuOutOfMemoryError\":[\"GpuError\"],\"GpuPipelineDescriptorBase\":[],\"GpuPipelineError\":[\"DomException\"],\"GpuPipelineErrorInit\":[],\"GpuPipelineErrorReason\":[],\"GpuPipelineLayout\":[],\"GpuPipelineLayoutDescriptor\":[],\"GpuPowerPreference\":[],\"GpuPrimitiveState\":[],\"GpuPrimitiveTopology\":[],\"GpuProgrammableStage\":[],\"GpuQuerySet\":[],\"GpuQuerySetDescriptor\":[],\"GpuQueryType\":[],\"GpuQueue\":[],\"GpuQueueDescriptor\":[],\"GpuRenderBundle\":[],\"GpuRenderBundleDescriptor\":[],\"GpuRenderBundleEncoder\":[],\"GpuRenderBundleEncoderDescriptor\":[],\"GpuRenderPassColorAttachment\":[],\"GpuRenderPassDepthStencilAttachment\":[],\"GpuRenderPassDescriptor\":[],\"GpuRenderPassEncoder\":[],\"GpuRenderPassLayout\":[],\"GpuRenderPassTimestampWrites\":[],\"GpuRenderPipeline\":[],\"GpuRenderPipelineDescriptor\":[],\"GpuRequestAdapterOptions\":[],\"GpuSampler\":[],\"GpuSamplerBindingLayout\":[],\"GpuSamplerBindingType\":[],\"GpuSamplerDescriptor\":[],\"GpuShaderModule\":[],\"GpuShaderModuleCompilationHint\":[],\"GpuShaderModuleDescriptor\":[],\"GpuStencilFaceState\":[],\"GpuStencilOperation\":[],\"GpuStorageTextureAccess\":[],\"GpuStorageTextureBindingLayout\":[],\"GpuStoreOp\":[],\"GpuSupportedFeatures\":[],\"GpuSupportedLimits\":[],\"GpuTexelCopyBufferInfo\":[],\"GpuTexelCopyBufferLayout\":[],\"GpuTexelCopyTextureInfo\":[],\"GpuTexture\":[],\"GpuTextureAspect\":[],\"GpuTextureBindingLayout\":[],\"GpuTextureDescriptor\":[],\"GpuTextureDimension\":[],\"GpuTextureFormat\":[],\"GpuTextureSampleType\":[],\"GpuTextureView\":[],\"GpuTextureViewDescriptor\":[],\"GpuTextureViewDimension\":[],\"GpuUncapturedErrorEvent\":[\"Event\"],\"GpuUncapturedErrorEventInit\":[],\"GpuValidationError\":[\"GpuError\"],\"GpuVertexAttribute\":[],\"GpuVertexBufferLayout\":[],\"GpuVertexFormat\":[],\"GpuVertexState\":[],\"GpuVertexStepMode\":[],\"GroupedHistoryEventInit\":[],\"HalfOpenInfoDict\":[],\"HardwareAcceleration\":[],\"HashChangeEvent\":[\"Event\"],\"HashChangeEventInit\":[],\"Headers\":[],\"HeadersGuardEnum\":[],\"Hid\":[\"EventTarget\"],\"HidCollectionInfo\":[],\"HidConnectionEvent\":[\"Event\"],\"HidConnectionEventInit\":[],\"HidDevice\":[\"EventTarget\"],\"HidDeviceFilter\":[],\"HidDeviceRequestOptions\":[],\"HidInputReportEvent\":[\"Event\"],\"HidInputReportEventInit\":[],\"HidReportInfo\":[],\"HidReportItem\":[],\"HidUnitSystem\":[],\"HiddenPluginEventInit\":[],\"History\":[],\"HitRegionOptions\":[],\"HkdfParams\":[],\"HmacDerivedKeyParams\":[],\"HmacImportParams\":[],\"HmacKeyAlgorithm\":[],\"HmacKeyGenParams\":[],\"HtmlAllCollection\":[],\"HtmlAnchorElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAudioElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HtmlBaseElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBodyElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlButtonElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCanvasElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCollection\":[],\"HtmlDListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDetailsElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDialogElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDirectoryElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDivElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"HtmlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"HtmlEmbedElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFieldSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFontElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFormControlsCollection\":[\"HtmlCollection\"],\"HtmlFormElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadingElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHtmlElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlIFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlImageElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlInputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLabelElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLegendElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLiElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLinkElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMapElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMediaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuItemElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMetaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMeterElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlModElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlObjectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptGroupElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionsCollection\":[\"HtmlCollection\"],\"HtmlOutputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParagraphElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParamElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPictureElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPreElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlProgressElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlQuoteElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlScriptElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSelectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSlotElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSourceElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSpanElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlStyleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCaptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCellElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableColElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableRowElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableSectionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTemplateElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTextAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTimeElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTitleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTrackElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUnknownElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlVideoElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HttpConnDict\":[],\"HttpConnInfo\":[],\"HttpConnectionElement\":[],\"IdbCursor\":[],\"IdbCursorDirection\":[],\"IdbCursorWithValue\":[\"IdbCursor\"],\"IdbDatabase\":[\"EventTarget\"],\"IdbFactory\":[],\"IdbFileHandle\":[\"EventTarget\"],\"IdbFileMetadataParameters\":[],\"IdbFileRequest\":[\"DomRequest\",\"EventTarget\"],\"IdbIndex\":[],\"IdbIndexParameters\":[],\"IdbKeyRange\":[],\"IdbLocaleAwareKeyRange\":[\"IdbKeyRange\"],\"IdbMutableFile\":[\"EventTarget\"],\"IdbObjectStore\":[],\"IdbObjectStoreParameters\":[],\"IdbOpenDbOptions\":[],\"IdbOpenDbRequest\":[\"EventTarget\",\"IdbRequest\"],\"IdbRequest\":[\"EventTarget\"],\"IdbRequestReadyState\":[],\"IdbTransaction\":[\"EventTarget\"],\"IdbTransactionDurability\":[],\"IdbTransactionMode\":[],\"IdbTransactionOptions\":[],\"IdbVersionChangeEvent\":[\"Event\"],\"IdbVersionChangeEventInit\":[],\"IdleDeadline\":[],\"IdleRequestOptions\":[],\"IirFilterNode\":[\"AudioNode\",\"EventTarget\"],\"IirFilterOptions\":[],\"ImageBitmap\":[],\"ImageBitmapOptions\":[],\"ImageBitmapRenderingContext\":[],\"ImageCapture\":[],\"ImageCaptureError\":[],\"ImageCaptureErrorEvent\":[\"Event\"],\"ImageCaptureErrorEventInit\":[],\"ImageData\":[],\"ImageDecodeOptions\":[],\"ImageDecodeResult\":[],\"ImageDecoder\":[],\"ImageDecoderInit\":[],\"ImageEncodeOptions\":[],\"ImageOrientation\":[],\"ImageTrack\":[\"EventTarget\"],\"ImageTrackList\":[],\"InputDeviceInfo\":[\"MediaDeviceInfo\"],\"InputEvent\":[\"Event\",\"UiEvent\"],\"InputEventInit\":[],\"IntersectionObserver\":[],\"IntersectionObserverEntry\":[],\"IntersectionObserverEntryInit\":[],\"IntersectionObserverInit\":[],\"IntlUtils\":[],\"IsInputPendingOptions\":[],\"IterableKeyAndValueResult\":[],\"IterableKeyOrValueResult\":[],\"IterationCompositeOperation\":[],\"JsonWebKey\":[],\"KeyAlgorithm\":[],\"KeyEvent\":[],\"KeyFrameRequestEvent\":[\"Event\"],\"KeyIdsInitData\":[],\"KeyboardEvent\":[\"Event\",\"UiEvent\"],\"KeyboardEventInit\":[],\"KeyframeAnimationOptions\":[],\"KeyframeEffect\":[\"AnimationEffect\"],\"KeyframeEffectOptions\":[],\"L10nElement\":[],\"L10nValue\":[],\"LargeBlobSupport\":[],\"LatencyMode\":[],\"LifecycleCallbacks\":[],\"LineAlignSetting\":[],\"ListBoxObject\":[],\"LocalMediaStream\":[\"EventTarget\",\"MediaStream\"],\"LocaleInfo\":[],\"Location\":[],\"Lock\":[],\"LockInfo\":[],\"LockManager\":[],\"LockManagerSnapshot\":[],\"LockMode\":[],\"LockOptions\":[],\"MathMlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"MediaCapabilities\":[],\"MediaCapabilitiesInfo\":[],\"MediaConfiguration\":[],\"MediaDecodingConfiguration\":[],\"MediaDecodingType\":[],\"MediaDeviceInfo\":[],\"MediaDeviceKind\":[],\"MediaDevices\":[\"EventTarget\"],\"MediaElementAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaElementAudioSourceOptions\":[],\"MediaEncodingConfiguration\":[],\"MediaEncodingType\":[],\"MediaEncryptedEvent\":[\"Event\"],\"MediaError\":[],\"MediaImage\":[],\"MediaKeyError\":[\"Event\"],\"MediaKeyMessageEvent\":[\"Event\"],\"MediaKeyMessageEventInit\":[],\"MediaKeyMessageType\":[],\"MediaKeyNeededEventInit\":[],\"MediaKeySession\":[\"EventTarget\"],\"MediaKeySessionType\":[],\"MediaKeyStatus\":[],\"MediaKeyStatusMap\":[],\"MediaKeySystemAccess\":[],\"MediaKeySystemConfiguration\":[],\"MediaKeySystemMediaCapability\":[],\"MediaKeySystemStatus\":[],\"MediaKeys\":[],\"MediaKeysPolicy\":[],\"MediaKeysRequirement\":[],\"MediaList\":[],\"MediaMetadata\":[],\"MediaMetadataInit\":[],\"MediaPositionState\":[],\"MediaQueryList\":[\"EventTarget\"],\"MediaQueryListEvent\":[\"Event\"],\"MediaQueryListEventInit\":[],\"MediaRecorder\":[\"EventTarget\"],\"MediaRecorderErrorEvent\":[\"Event\"],\"MediaRecorderErrorEventInit\":[],\"MediaRecorderOptions\":[],\"MediaSession\":[],\"MediaSessionAction\":[],\"MediaSessionActionDetails\":[],\"MediaSessionPlaybackState\":[],\"MediaSource\":[\"EventTarget\"],\"MediaSourceEndOfStreamError\":[],\"MediaSourceEnum\":[],\"MediaSourceReadyState\":[],\"MediaStream\":[\"EventTarget\"],\"MediaStreamAudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceOptions\":[],\"MediaStreamConstraints\":[],\"MediaStreamError\":[],\"MediaStreamEvent\":[\"Event\"],\"MediaStreamEventInit\":[],\"MediaStreamTrack\":[\"EventTarget\"],\"MediaStreamTrackEvent\":[\"Event\"],\"MediaStreamTrackEventInit\":[],\"MediaStreamTrackGenerator\":[\"EventTarget\",\"MediaStreamTrack\"],\"MediaStreamTrackGeneratorInit\":[],\"MediaStreamTrackProcessor\":[],\"MediaStreamTrackProcessorInit\":[],\"MediaStreamTrackState\":[],\"MediaTrackCapabilities\":[],\"MediaTrackConstraintSet\":[],\"MediaTrackConstraints\":[],\"MediaTrackSettings\":[],\"MediaTrackSupportedConstraints\":[],\"MemoryAttribution\":[],\"MemoryAttributionContainer\":[],\"MemoryBreakdownEntry\":[],\"MemoryMeasurement\":[],\"MessageChannel\":[],\"MessageEvent\":[\"Event\"],\"MessageEventInit\":[],\"MessagePort\":[\"EventTarget\"],\"MidiAccess\":[\"EventTarget\"],\"MidiConnectionEvent\":[\"Event\"],\"MidiConnectionEventInit\":[],\"MidiInput\":[\"EventTarget\",\"MidiPort\"],\"MidiInputMap\":[],\"MidiMessageEvent\":[\"Event\"],\"MidiMessageEventInit\":[],\"MidiOptions\":[],\"MidiOutput\":[\"EventTarget\",\"MidiPort\"],\"MidiOutputMap\":[],\"MidiPort\":[\"EventTarget\"],\"MidiPortConnectionState\":[],\"MidiPortDeviceState\":[],\"MidiPortType\":[],\"MimeType\":[],\"MimeTypeArray\":[],\"MouseEvent\":[\"Event\",\"UiEvent\"],\"MouseEventInit\":[],\"MouseScrollEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"MozDebug\":[],\"MutationEvent\":[\"Event\"],\"MutationObserver\":[],\"MutationObserverInit\":[],\"MutationObservingInfo\":[],\"MutationRecord\":[],\"NamedNodeMap\":[],\"NativeOsFileReadOptions\":[],\"NativeOsFileWriteAtomicOptions\":[],\"NavigationType\":[],\"Navigator\":[],\"NavigatorAutomationInformation\":[],\"NavigatorUaBrandVersion\":[],\"NavigatorUaData\":[],\"NetworkCommandOptions\":[],\"NetworkInformation\":[\"EventTarget\"],\"NetworkResultOptions\":[],\"Node\":[\"EventTarget\"],\"NodeFilter\":[],\"NodeIterator\":[],\"NodeList\":[],\"Notification\":[\"EventTarget\"],\"NotificationAction\":[],\"NotificationDirection\":[],\"NotificationEvent\":[\"Event\",\"ExtendableEvent\"],\"NotificationEventInit\":[],\"NotificationOptions\":[],\"NotificationPermission\":[],\"ObserverCallback\":[],\"OesElementIndexUint\":[],\"OesStandardDerivatives\":[],\"OesTextureFloat\":[],\"OesTextureFloatLinear\":[],\"OesTextureHalfFloat\":[],\"OesTextureHalfFloatLinear\":[],\"OesVertexArrayObject\":[],\"OfflineAudioCompletionEvent\":[\"Event\"],\"OfflineAudioCompletionEventInit\":[],\"OfflineAudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"OfflineAudioContextOptions\":[],\"OfflineResourceList\":[\"EventTarget\"],\"OffscreenCanvas\":[\"EventTarget\"],\"OffscreenCanvasRenderingContext2d\":[],\"OpenFilePickerOptions\":[],\"OpenWindowEventDetail\":[],\"OptionalEffectTiming\":[],\"OrientationLockType\":[],\"OrientationType\":[],\"OscillatorNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"OscillatorOptions\":[],\"OscillatorType\":[],\"OverSampleType\":[],\"OvrMultiview2\":[],\"PageTransitionEvent\":[\"Event\"],\"PageTransitionEventInit\":[],\"PaintRequest\":[],\"PaintRequestList\":[],\"PaintWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"PannerNode\":[\"AudioNode\",\"EventTarget\"],\"PannerOptions\":[],\"PanningModelType\":[],\"ParityType\":[],\"Path2d\":[],\"PaymentAddress\":[],\"PaymentComplete\":[],\"PaymentMethodChangeEvent\":[\"Event\",\"PaymentRequestUpdateEvent\"],\"PaymentMethodChangeEventInit\":[],\"PaymentRequestUpdateEvent\":[\"Event\"],\"PaymentRequestUpdateEventInit\":[],\"PaymentResponse\":[],\"Pbkdf2Params\":[],\"PcImplIceConnectionState\":[],\"PcImplIceGatheringState\":[],\"PcImplSignalingState\":[],\"PcObserverStateType\":[],\"Performance\":[\"EventTarget\"],\"PerformanceEntry\":[],\"PerformanceEntryEventInit\":[],\"PerformanceEntryFilterOptions\":[],\"PerformanceMark\":[\"PerformanceEntry\"],\"PerformanceMeasure\":[\"PerformanceEntry\"],\"PerformanceNavigation\":[],\"PerformanceNavigationTiming\":[\"PerformanceEntry\",\"PerformanceResourceTiming\"],\"PerformanceObserver\":[],\"PerformanceObserverEntryList\":[],\"PerformanceObserverInit\":[],\"PerformanceResourceTiming\":[\"PerformanceEntry\"],\"PerformanceServerTiming\":[],\"PerformanceTiming\":[],\"PeriodicWave\":[],\"PeriodicWaveConstraints\":[],\"PeriodicWaveOptions\":[],\"PermissionDescriptor\":[],\"PermissionName\":[],\"PermissionState\":[],\"PermissionStatus\":[\"EventTarget\"],\"Permissions\":[],\"PictureInPictureEvent\":[\"Event\"],\"PictureInPictureEventInit\":[],\"PictureInPictureWindow\":[\"EventTarget\"],\"PlaneLayout\":[],\"PlaybackDirection\":[],\"Plugin\":[],\"PluginArray\":[],\"PluginCrashedEventInit\":[],\"PointerEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"PointerEventInit\":[],\"PopStateEvent\":[\"Event\"],\"PopStateEventInit\":[],\"PopupBlockedEvent\":[\"Event\"],\"PopupBlockedEventInit\":[],\"Position\":[],\"PositionAlignSetting\":[],\"PositionError\":[],\"PositionOptions\":[],\"PremultiplyAlpha\":[],\"Presentation\":[],\"PresentationAvailability\":[\"EventTarget\"],\"PresentationConnection\":[\"EventTarget\"],\"PresentationConnectionAvailableEvent\":[\"Event\"],\"PresentationConnectionAvailableEventInit\":[],\"PresentationConnectionBinaryType\":[],\"PresentationConnectionCloseEvent\":[\"Event\"],\"PresentationConnectionCloseEventInit\":[],\"PresentationConnectionClosedReason\":[],\"PresentationConnectionList\":[\"EventTarget\"],\"PresentationConnectionState\":[],\"PresentationReceiver\":[],\"PresentationRequest\":[\"EventTarget\"],\"PresentationStyle\":[],\"ProcessingInstruction\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"ProfileTimelineLayerRect\":[],\"ProfileTimelineMarker\":[],\"ProfileTimelineMessagePortOperationType\":[],\"ProfileTimelineStackFrame\":[],\"ProfileTimelineWorkerOperationType\":[],\"ProgressEvent\":[\"Event\"],\"ProgressEventInit\":[],\"PromiseNativeHandler\":[],\"PromiseRejectionEvent\":[\"Event\"],\"PromiseRejectionEventInit\":[],\"PublicKeyCredential\":[\"Credential\"],\"PublicKeyCredentialCreationOptions\":[],\"PublicKeyCredentialCreationOptionsJson\":[],\"PublicKeyCredentialDescriptor\":[],\"PublicKeyCredentialDescriptorJson\":[],\"PublicKeyCredentialEntity\":[],\"PublicKeyCredentialHints\":[],\"PublicKeyCredentialParameters\":[],\"PublicKeyCredentialRequestOptions\":[],\"PublicKeyCredentialRequestOptionsJson\":[],\"PublicKeyCredentialRpEntity\":[],\"PublicKeyCredentialType\":[],\"PublicKeyCredentialUserEntity\":[],\"PublicKeyCredentialUserEntityJson\":[],\"PushEncryptionKeyName\":[],\"PushEvent\":[\"Event\",\"ExtendableEvent\"],\"PushEventInit\":[],\"PushManager\":[],\"PushMessageData\":[],\"PushPermissionState\":[],\"PushSubscription\":[],\"PushSubscriptionInit\":[],\"PushSubscriptionJson\":[],\"PushSubscriptionKeys\":[],\"PushSubscriptionOptions\":[],\"PushSubscriptionOptionsInit\":[],\"QueryOptions\":[],\"QueuingStrategy\":[],\"QueuingStrategyInit\":[],\"RadioNodeList\":[\"NodeList\"],\"Range\":[],\"RcwnPerfStats\":[],\"RcwnStatus\":[],\"ReadableByteStreamController\":[],\"ReadableStream\":[],\"ReadableStreamByobReader\":[],\"ReadableStreamByobRequest\":[],\"ReadableStreamDefaultController\":[],\"ReadableStreamDefaultReader\":[],\"ReadableStreamGetReaderOptions\":[],\"ReadableStreamIteratorOptions\":[],\"ReadableStreamReadResult\":[],\"ReadableStreamReaderMode\":[],\"ReadableStreamType\":[],\"ReadableWritablePair\":[],\"RecordingState\":[],\"ReferrerPolicy\":[],\"RegisterRequest\":[],\"RegisterResponse\":[],\"RegisteredKey\":[],\"RegistrationOptions\":[],\"RegistrationResponseJson\":[],\"Request\":[],\"RequestCache\":[],\"RequestCredentials\":[],\"RequestDestination\":[],\"RequestDeviceOptions\":[],\"RequestInit\":[],\"RequestMediaKeySystemAccessNotification\":[],\"RequestMode\":[],\"RequestRedirect\":[],\"ResidentKeyRequirement\":[],\"ResizeObserver\":[],\"ResizeObserverBoxOptions\":[],\"ResizeObserverEntry\":[],\"ResizeObserverOptions\":[],\"ResizeObserverSize\":[],\"ResizeQuality\":[],\"Response\":[],\"ResponseInit\":[],\"ResponseType\":[],\"RsaHashedImportParams\":[],\"RsaOaepParams\":[],\"RsaOtherPrimesInfo\":[],\"RsaPssParams\":[],\"RtcAnswerOptions\":[],\"RtcBundlePolicy\":[],\"RtcCertificate\":[],\"RtcCertificateExpiration\":[],\"RtcCodecStats\":[],\"RtcConfiguration\":[],\"RtcDataChannel\":[\"EventTarget\"],\"RtcDataChannelEvent\":[\"Event\"],\"RtcDataChannelEventInit\":[],\"RtcDataChannelInit\":[],\"RtcDataChannelState\":[],\"RtcDataChannelType\":[],\"RtcDegradationPreference\":[],\"RtcEncodedAudioFrame\":[],\"RtcEncodedAudioFrameMetadata\":[],\"RtcEncodedAudioFrameOptions\":[],\"RtcEncodedVideoFrame\":[],\"RtcEncodedVideoFrameMetadata\":[],\"RtcEncodedVideoFrameOptions\":[],\"RtcEncodedVideoFrameType\":[],\"RtcFecParameters\":[],\"RtcIceCandidate\":[],\"RtcIceCandidateInit\":[],\"RtcIceCandidatePairStats\":[],\"RtcIceCandidateStats\":[],\"RtcIceComponentStats\":[],\"RtcIceConnectionState\":[],\"RtcIceCredentialType\":[],\"RtcIceGatheringState\":[],\"RtcIceServer\":[],\"RtcIceTransportPolicy\":[],\"RtcIdentityAssertion\":[],\"RtcIdentityAssertionResult\":[],\"RtcIdentityProvider\":[],\"RtcIdentityProviderDetails\":[],\"RtcIdentityProviderOptions\":[],\"RtcIdentityProviderRegistrar\":[],\"RtcIdentityValidationResult\":[],\"RtcInboundRtpStreamStats\":[],\"RtcMediaStreamStats\":[],\"RtcMediaStreamTrackStats\":[],\"RtcOfferAnswerOptions\":[],\"RtcOfferOptions\":[],\"RtcOutboundRtpStreamStats\":[],\"RtcPeerConnection\":[\"EventTarget\"],\"RtcPeerConnectionIceErrorEvent\":[\"Event\"],\"RtcPeerConnectionIceEvent\":[\"Event\"],\"RtcPeerConnectionIceEventInit\":[],\"RtcPeerConnectionState\":[],\"RtcPriorityType\":[],\"RtcRtcpParameters\":[],\"RtcRtpCapabilities\":[],\"RtcRtpCodecCapability\":[],\"RtcRtpCodecParameters\":[],\"RtcRtpContributingSource\":[],\"RtcRtpEncodingParameters\":[],\"RtcRtpHeaderExtensionCapability\":[],\"RtcRtpHeaderExtensionParameters\":[],\"RtcRtpParameters\":[],\"RtcRtpReceiver\":[],\"RtcRtpScriptTransform\":[],\"RtcRtpScriptTransformer\":[\"EventTarget\"],\"RtcRtpSender\":[],\"RtcRtpSourceEntry\":[],\"RtcRtpSourceEntryType\":[],\"RtcRtpSynchronizationSource\":[],\"RtcRtpTransceiver\":[],\"RtcRtpTransceiverDirection\":[],\"RtcRtpTransceiverInit\":[],\"RtcRtxParameters\":[],\"RtcSdpType\":[],\"RtcSessionDescription\":[],\"RtcSessionDescriptionInit\":[],\"RtcSignalingState\":[],\"RtcStats\":[],\"RtcStatsIceCandidatePairState\":[],\"RtcStatsIceCandidateType\":[],\"RtcStatsReport\":[],\"RtcStatsReportInternal\":[],\"RtcStatsType\":[],\"RtcTrackEvent\":[\"Event\"],\"RtcTrackEventInit\":[],\"RtcTransformEvent\":[\"Event\"],\"RtcTransportStats\":[],\"RtcdtmfSender\":[\"EventTarget\"],\"RtcdtmfToneChangeEvent\":[\"Event\"],\"RtcdtmfToneChangeEventInit\":[],\"RtcrtpContributingSourceStats\":[],\"RtcrtpStreamStats\":[],\"SFrameTransform\":[\"EventTarget\"],\"SFrameTransformErrorEvent\":[\"Event\"],\"SFrameTransformErrorEventInit\":[],\"SFrameTransformErrorEventType\":[],\"SFrameTransformOptions\":[],\"SFrameTransformRole\":[],\"SaveFilePickerOptions\":[],\"Scheduler\":[],\"SchedulerPostTaskOptions\":[],\"Scheduling\":[],\"Screen\":[\"EventTarget\"],\"ScreenColorGamut\":[],\"ScreenDetailed\":[\"EventTarget\",\"Screen\"],\"ScreenDetails\":[\"EventTarget\"],\"ScreenLuminance\":[],\"ScreenOrientation\":[\"EventTarget\"],\"ScriptProcessorNode\":[\"AudioNode\",\"EventTarget\"],\"ScrollAreaEvent\":[\"Event\",\"UiEvent\"],\"ScrollBehavior\":[],\"ScrollBoxObject\":[],\"ScrollIntoViewContainer\":[],\"ScrollIntoViewOptions\":[],\"ScrollLogicalPosition\":[],\"ScrollOptions\":[],\"ScrollRestoration\":[],\"ScrollSetting\":[],\"ScrollState\":[],\"ScrollToOptions\":[],\"ScrollViewChangeEventInit\":[],\"SecurityPolicyViolationEvent\":[\"Event\"],\"SecurityPolicyViolationEventDisposition\":[],\"SecurityPolicyViolationEventInit\":[],\"Selection\":[],\"SelectionMode\":[],\"Serial\":[\"EventTarget\"],\"SerialInputSignals\":[],\"SerialOptions\":[],\"SerialOutputSignals\":[],\"SerialPort\":[\"EventTarget\"],\"SerialPortFilter\":[],\"SerialPortInfo\":[],\"SerialPortRequestOptions\":[],\"ServerSocketOptions\":[],\"ServiceWorker\":[\"EventTarget\"],\"ServiceWorkerContainer\":[\"EventTarget\"],\"ServiceWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"ServiceWorkerRegistration\":[\"EventTarget\"],\"ServiceWorkerState\":[],\"ServiceWorkerUpdateViaCache\":[],\"ShadowRoot\":[\"DocumentFragment\",\"EventTarget\",\"Node\"],\"ShadowRootInit\":[],\"ShadowRootMode\":[],\"ShareData\":[],\"SharedWorker\":[\"EventTarget\"],\"SharedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"SignResponse\":[],\"SocketElement\":[],\"SocketOptions\":[],\"SocketReadyState\":[],\"SocketsDict\":[],\"SourceBuffer\":[\"EventTarget\"],\"SourceBufferAppendMode\":[],\"SourceBufferList\":[\"EventTarget\"],\"SpeechGrammar\":[],\"SpeechGrammarList\":[],\"SpeechRecognition\":[\"EventTarget\"],\"SpeechRecognitionAlternative\":[],\"SpeechRecognitionError\":[\"Event\"],\"SpeechRecognitionErrorCode\":[],\"SpeechRecognitionErrorInit\":[],\"SpeechRecognitionEvent\":[\"Event\"],\"SpeechRecognitionEventInit\":[],\"SpeechRecognitionResult\":[],\"SpeechRecognitionResultList\":[],\"SpeechSynthesis\":[\"EventTarget\"],\"SpeechSynthesisErrorCode\":[],\"SpeechSynthesisErrorEvent\":[\"Event\",\"SpeechSynthesisEvent\"],\"SpeechSynthesisErrorEventInit\":[],\"SpeechSynthesisEvent\":[\"Event\"],\"SpeechSynthesisEventInit\":[],\"SpeechSynthesisUtterance\":[\"EventTarget\"],\"SpeechSynthesisVoice\":[],\"StereoPannerNode\":[\"AudioNode\",\"EventTarget\"],\"StereoPannerOptions\":[],\"Storage\":[],\"StorageEstimate\":[],\"StorageEvent\":[\"Event\"],\"StorageEventInit\":[],\"StorageManager\":[],\"StorageType\":[],\"StreamPipeOptions\":[],\"StyleRuleChangeEventInit\":[],\"StyleSheet\":[],\"StyleSheetApplicableStateChangeEventInit\":[],\"StyleSheetChangeEventInit\":[],\"StyleSheetList\":[],\"SubmitEvent\":[\"Event\"],\"SubmitEventInit\":[],\"SubtleCrypto\":[],\"SupportedType\":[],\"SvcOutputMetadata\":[],\"SvgAngle\":[],\"SvgAnimateElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateMotionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateTransformElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimatedAngle\":[],\"SvgAnimatedBoolean\":[],\"SvgAnimatedEnumeration\":[],\"SvgAnimatedInteger\":[],\"SvgAnimatedLength\":[],\"SvgAnimatedLengthList\":[],\"SvgAnimatedNumber\":[],\"SvgAnimatedNumberList\":[],\"SvgAnimatedPreserveAspectRatio\":[],\"SvgAnimatedRect\":[],\"SvgAnimatedString\":[],\"SvgAnimatedTransformList\":[],\"SvgAnimationElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgBoundingBoxOptions\":[],\"SvgCircleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgClipPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgComponentTransferFunctionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgDefsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgDescElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgElement\":[\"Element\",\"EventTarget\",\"Node\"],\"SvgEllipseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgFilterElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgForeignObjectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGeometryElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgGraphicsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgLength\":[],\"SvgLengthList\":[],\"SvgLineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgLinearGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgMarkerElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMaskElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMatrix\":[],\"SvgMetadataElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgNumber\":[],\"SvgNumberList\":[],\"SvgPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPathSeg\":[],\"SvgPathSegArcAbs\":[\"SvgPathSeg\"],\"SvgPathSegArcRel\":[\"SvgPathSeg\"],\"SvgPathSegClosePath\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalRel\":[\"SvgPathSeg\"],\"SvgPathSegList\":[],\"SvgPathSegMovetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegMovetoRel\":[\"SvgPathSeg\"],\"SvgPatternElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgPoint\":[],\"SvgPointList\":[],\"SvgPolygonElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPolylineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPreserveAspectRatio\":[],\"SvgRadialGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgRect\":[],\"SvgRectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgScriptElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgStopElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgStringList\":[],\"SvgStyleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSwitchElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgSymbolElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTextContentElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgTextElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"SvgTextPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTextPositioningElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTitleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTransform\":[],\"SvgTransformList\":[],\"SvgUnitTypes\":[],\"SvgUseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgViewElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgZoomAndPan\":[],\"SvgaElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgfeBlendElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeColorMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeComponentTransferElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeCompositeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeConvolveMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDiffuseLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDisplacementMapElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDistantLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDropShadowElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFloodElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFuncAElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncBElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncGElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncRElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeGaussianBlurElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeNodeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMorphologyElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeOffsetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfePointLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpecularLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpotLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTileElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTurbulenceElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvggElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgmPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgsvgElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgtSpanElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"TaskController\":[\"AbortController\"],\"TaskControllerInit\":[],\"TaskPriority\":[],\"TaskPriorityChangeEvent\":[\"Event\"],\"TaskPriorityChangeEventInit\":[],\"TaskSignal\":[\"AbortSignal\",\"EventTarget\"],\"TaskSignalAnyInit\":[],\"TcpReadyState\":[],\"TcpServerSocket\":[\"EventTarget\"],\"TcpServerSocketEvent\":[\"Event\"],\"TcpServerSocketEventInit\":[],\"TcpSocket\":[\"EventTarget\"],\"TcpSocketBinaryType\":[],\"TcpSocketErrorEvent\":[\"Event\"],\"TcpSocketErrorEventInit\":[],\"TcpSocketEvent\":[\"Event\"],\"TcpSocketEventInit\":[],\"Text\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"TextDecodeOptions\":[],\"TextDecoder\":[],\"TextDecoderOptions\":[],\"TextEncoder\":[],\"TextMetrics\":[],\"TextTrack\":[\"EventTarget\"],\"TextTrackCue\":[\"EventTarget\"],\"TextTrackCueList\":[],\"TextTrackKind\":[],\"TextTrackList\":[\"EventTarget\"],\"TextTrackMode\":[],\"TimeEvent\":[\"Event\"],\"TimeRanges\":[],\"ToggleEvent\":[\"Event\"],\"ToggleEventInit\":[],\"TokenBinding\":[],\"TokenBindingStatus\":[],\"Touch\":[],\"TouchEvent\":[\"Event\",\"UiEvent\"],\"TouchEventInit\":[],\"TouchInit\":[],\"TouchList\":[],\"TrackEvent\":[\"Event\"],\"TrackEventInit\":[],\"TransformStream\":[],\"TransformStreamDefaultController\":[],\"Transformer\":[],\"TransitionEvent\":[\"Event\"],\"TransitionEventInit\":[],\"Transport\":[],\"TreeBoxObject\":[],\"TreeCellInfo\":[],\"TreeView\":[],\"TreeWalker\":[],\"U2f\":[],\"U2fClientData\":[],\"ULongRange\":[],\"UaDataValues\":[],\"UaLowEntropyJson\":[],\"UdpMessageEventInit\":[],\"UdpOptions\":[],\"UiEvent\":[\"Event\"],\"UiEventInit\":[],\"UnderlyingSink\":[],\"UnderlyingSource\":[],\"Url\":[],\"UrlSearchParams\":[],\"Usb\":[\"EventTarget\"],\"UsbAlternateInterface\":[],\"UsbConfiguration\":[],\"UsbConnectionEvent\":[\"Event\"],\"UsbConnectionEventInit\":[],\"UsbControlTransferParameters\":[],\"UsbDevice\":[],\"UsbDeviceFilter\":[],\"UsbDeviceRequestOptions\":[],\"UsbDirection\":[],\"UsbEndpoint\":[],\"UsbEndpointType\":[],\"UsbInTransferResult\":[],\"UsbInterface\":[],\"UsbIsochronousInTransferPacket\":[],\"UsbIsochronousInTransferResult\":[],\"UsbIsochronousOutTransferPacket\":[],\"UsbIsochronousOutTransferResult\":[],\"UsbOutTransferResult\":[],\"UsbPermissionDescriptor\":[],\"UsbPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"UsbPermissionStorage\":[],\"UsbRecipient\":[],\"UsbRequestType\":[],\"UsbTransferStatus\":[],\"UserActivation\":[],\"UserProximityEvent\":[\"Event\"],\"UserProximityEventInit\":[],\"UserVerificationRequirement\":[],\"ValidityState\":[],\"ValueEvent\":[\"Event\"],\"ValueEventInit\":[],\"VideoColorPrimaries\":[],\"VideoColorSpace\":[],\"VideoColorSpaceInit\":[],\"VideoConfiguration\":[],\"VideoDecoder\":[],\"VideoDecoderConfig\":[],\"VideoDecoderInit\":[],\"VideoDecoderSupport\":[],\"VideoEncoder\":[],\"VideoEncoderConfig\":[],\"VideoEncoderEncodeOptions\":[],\"VideoEncoderInit\":[],\"VideoEncoderSupport\":[],\"VideoFacingModeEnum\":[],\"VideoFrame\":[],\"VideoFrameBufferInit\":[],\"VideoFrameCopyToOptions\":[],\"VideoFrameInit\":[],\"VideoMatrixCoefficients\":[],\"VideoPixelFormat\":[],\"VideoPlaybackQuality\":[],\"VideoStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"VideoTrack\":[],\"VideoTrackList\":[\"EventTarget\"],\"VideoTransferCharacteristics\":[],\"ViewTransition\":[],\"VisibilityState\":[],\"VisualViewport\":[\"EventTarget\"],\"VoidCallback\":[],\"VrDisplay\":[\"EventTarget\"],\"VrDisplayCapabilities\":[],\"VrEye\":[],\"VrEyeParameters\":[],\"VrFieldOfView\":[],\"VrFrameData\":[],\"VrLayer\":[],\"VrMockController\":[],\"VrMockDisplay\":[],\"VrPose\":[],\"VrServiceTest\":[],\"VrStageParameters\":[],\"VrSubmitFrameResult\":[],\"VttCue\":[\"EventTarget\",\"TextTrackCue\"],\"VttRegion\":[],\"WakeLock\":[],\"WakeLockSentinel\":[\"EventTarget\"],\"WakeLockType\":[],\"WatchAdvertisementsOptions\":[],\"WaveShaperNode\":[\"AudioNode\",\"EventTarget\"],\"WaveShaperOptions\":[],\"WebGl2RenderingContext\":[],\"WebGlActiveInfo\":[],\"WebGlBuffer\":[],\"WebGlContextAttributes\":[],\"WebGlContextEvent\":[\"Event\"],\"WebGlContextEventInit\":[],\"WebGlFramebuffer\":[],\"WebGlPowerPreference\":[],\"WebGlProgram\":[],\"WebGlQuery\":[],\"WebGlRenderbuffer\":[],\"WebGlRenderingContext\":[],\"WebGlSampler\":[],\"WebGlShader\":[],\"WebGlShaderPrecisionFormat\":[],\"WebGlSync\":[],\"WebGlTexture\":[],\"WebGlTransformFeedback\":[],\"WebGlUniformLocation\":[],\"WebGlVertexArrayObject\":[],\"WebKitCssMatrix\":[\"DomMatrix\",\"DomMatrixReadOnly\"],\"WebSocket\":[\"EventTarget\"],\"WebSocketDict\":[],\"WebSocketElement\":[],\"WebTransport\":[],\"WebTransportBidirectionalStream\":[],\"WebTransportCloseInfo\":[],\"WebTransportCongestionControl\":[],\"WebTransportDatagramDuplexStream\":[],\"WebTransportDatagramStats\":[],\"WebTransportError\":[\"DomException\"],\"WebTransportErrorOptions\":[],\"WebTransportErrorSource\":[],\"WebTransportHash\":[],\"WebTransportOptions\":[],\"WebTransportReceiveStream\":[\"ReadableStream\"],\"WebTransportReceiveStreamStats\":[],\"WebTransportReliabilityMode\":[],\"WebTransportSendStream\":[\"WritableStream\"],\"WebTransportSendStreamOptions\":[],\"WebTransportSendStreamStats\":[],\"WebTransportStats\":[],\"WebglColorBufferFloat\":[],\"WebglCompressedTextureAstc\":[],\"WebglCompressedTextureAtc\":[],\"WebglCompressedTextureEtc\":[],\"WebglCompressedTextureEtc1\":[],\"WebglCompressedTexturePvrtc\":[],\"WebglCompressedTextureS3tc\":[],\"WebglCompressedTextureS3tcSrgb\":[],\"WebglDebugRendererInfo\":[],\"WebglDebugShaders\":[],\"WebglDepthTexture\":[],\"WebglDrawBuffers\":[],\"WebglLoseContext\":[],\"WebglMultiDraw\":[],\"WellKnownDirectory\":[],\"WgslLanguageFeatures\":[],\"WheelEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"WheelEventInit\":[],\"WidevineCdmManifest\":[],\"Window\":[\"EventTarget\"],\"WindowClient\":[\"Client\"],\"Worker\":[\"EventTarget\"],\"WorkerDebuggerGlobalScope\":[\"EventTarget\"],\"WorkerGlobalScope\":[\"EventTarget\"],\"WorkerLocation\":[],\"WorkerNavigator\":[],\"WorkerOptions\":[],\"WorkerType\":[],\"Worklet\":[],\"WorkletGlobalScope\":[],\"WorkletOptions\":[],\"WritableStream\":[],\"WritableStreamDefaultController\":[],\"WritableStreamDefaultWriter\":[],\"WriteCommandType\":[],\"WriteParams\":[],\"XPathExpression\":[],\"XPathNsResolver\":[],\"XPathResult\":[],\"XmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"XmlHttpRequest\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlHttpRequestEventTarget\":[\"EventTarget\"],\"XmlHttpRequestResponseType\":[],\"XmlHttpRequestUpload\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlSerializer\":[],\"XrBoundedReferenceSpace\":[\"EventTarget\",\"XrReferenceSpace\",\"XrSpace\"],\"XrEye\":[],\"XrFrame\":[],\"XrHand\":[],\"XrHandJoint\":[],\"XrHandedness\":[],\"XrInputSource\":[],\"XrInputSourceArray\":[],\"XrInputSourceEvent\":[\"Event\"],\"XrInputSourceEventInit\":[],\"XrInputSourcesChangeEvent\":[\"Event\"],\"XrInputSourcesChangeEventInit\":[],\"XrJointPose\":[\"XrPose\"],\"XrJointSpace\":[\"EventTarget\",\"XrSpace\"],\"XrLayer\":[\"EventTarget\"],\"XrPermissionDescriptor\":[],\"XrPermissionStatus\":[\"EventTarget\",\"PermissionStatus\"],\"XrPose\":[],\"XrReferenceSpace\":[\"EventTarget\",\"XrSpace\"],\"XrReferenceSpaceEvent\":[\"Event\"],\"XrReferenceSpaceEventInit\":[],\"XrReferenceSpaceType\":[],\"XrRenderState\":[],\"XrRenderStateInit\":[],\"XrRigidTransform\":[],\"XrSession\":[\"EventTarget\"],\"XrSessionEvent\":[\"Event\"],\"XrSessionEventInit\":[],\"XrSessionInit\":[],\"XrSessionMode\":[],\"XrSessionSupportedPermissionDescriptor\":[],\"XrSpace\":[\"EventTarget\"],\"XrSystem\":[\"EventTarget\"],\"XrTargetRayMode\":[],\"XrView\":[],\"XrViewerPose\":[\"XrPose\"],\"XrViewport\":[],\"XrVisibilityState\":[],\"XrWebGlLayer\":[\"EventTarget\",\"XrLayer\"],\"XrWebGlLayerInit\":[],\"XsltProcessor\":[],\"console\":[],\"css\":[],\"default\":[\"std\"],\"gpu_buffer_usage\":[],\"gpu_color_write\":[],\"gpu_map_mode\":[],\"gpu_shader_stage\":[],\"gpu_texture_usage\":[],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\"]}}", "web-time_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.20\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"macro\"],\"kind\":\"dev\",\"name\":\"pollster\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"^0.2.70\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"features\":[\"WorkerGlobalScope\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"CssStyleDeclaration\",\"Document\",\"Element\",\"HtmlTableElement\",\"HtmlTableRowElement\",\"Performance\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", - "webbrowser_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.0\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"default_features\":false,\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\">=0.3, <=0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"NSDictionary\",\"NSString\",\"NSURL\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^0.10\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"url\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"Window\"],\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"disable-wsl\":[],\"hardened\":[],\"wasm-console\":[\"web-sys/console\"]}}", - "webpki-root-certs_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aws-lc-rs\",\"req\":\"^1.15.2\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", + "webbrowser_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.0\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\">=0.3, <=0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"NSDictionary\",\"NSString\",\"NSURL\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^0.10\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"Window\"],\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"disable-wsl\":[],\"hardened\":[],\"wasm-console\":[\"web-sys/console\"]}}", + "webpki-root-certs_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"}],\"features\":{}}", "webpki-roots_0.26.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"parent\",\"package\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", - "webpki-roots_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aws-lc-rs\",\"req\":\"^1.15.2\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", + "webpki-roots_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "weezl_0.1.12": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.12\"},{\"default_features\":false,\"features\":[\"macros\",\"io-util\",\"net\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"compat\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.2\"}],\"features\":{\"alloc\":[],\"async\":[\"futures\",\"std\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "which_8.0.0": "{\"dependencies\":[{\"name\":\"env_home\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"optional\":true,\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"optional\":true,\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"real-sys\"],\"real-sys\":[\"dep:env_home\",\"dep:rustix\",\"dep:winsafe\"],\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "whoami_1.6.1": "{\"dependencies\":[{\"name\":\"libredox\",\"req\":\"^0.1.1\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"wasite\",\"req\":\"^0.1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"web\"],\"web\":[\"web-sys\"]}}", @@ -1480,12 +1515,7 @@ "winsafe_0.0.19": "{\"dependencies\":[],\"features\":{\"comctl\":[\"ole\"],\"dshow\":[\"oleaut\"],\"dwm\":[\"uxtheme\"],\"dxgi\":[\"ole\"],\"gdi\":[\"user\"],\"gui\":[\"comctl\",\"shell\",\"uxtheme\"],\"kernel\":[],\"mf\":[\"oleaut\"],\"ole\":[\"user\"],\"oleaut\":[\"ole\"],\"shell\":[\"oleaut\"],\"taskschd\":[\"oleaut\"],\"user\":[\"kernel\"],\"uxtheme\":[\"gdi\",\"ole\"],\"version\":[\"kernel\"]}}", "winsplit_0.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "wiremock_0.6.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-rt\",\"req\":\"^2.10.0\"},{\"name\":\"assert-json-diff\",\"req\":\"^2.0.2\"},{\"features\":[\"attributes\",\"tokio1\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.2\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"deadpool\",\"req\":\"^0.12.2\"},{\"name\":\"futures\",\"req\":\"^0.3.31\"},{\"name\":\"http\",\"req\":\"^1.3\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"full\"],\"name\":\"hyper\",\"req\":\"^1.7\"},{\"features\":[\"tokio\",\"server\",\"http1\",\"http2\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"regex\",\"req\":\"^1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12.23\"},{\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"rt\",\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.47.1\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.47.1\"},{\"name\":\"url\",\"req\":\"^2.5\"}],\"features\":{}}", - "wit-bindgen-core_0.51.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.72\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.3.19\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.218\"},{\"name\":\"wit-parser\",\"req\":\"^0.244.0\"}],\"features\":{\"clap\":[\"dep:clap\"],\"serde\":[\"dep:serde\"]}}", - "wit-bindgen-rust-macro_0.51.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.72\"},{\"name\":\"prettyplease\",\"req\":\"^0.2.20\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.89\"},{\"name\":\"wit-bindgen-core\",\"req\":\"^0.51.0\"},{\"name\":\"wit-bindgen-rust\",\"req\":\"^0.51.0\"}],\"features\":{\"async\":[]}}", - "wit-bindgen-rust_0.51.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.72\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.3.19\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.31\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"indexmap\",\"req\":\"^2.0.0\"},{\"name\":\"prettyplease\",\"req\":\"^0.2.20\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.218\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.89\"},{\"default_features\":false,\"name\":\"wasm-metadata\",\"req\":\"^0.244.0\"},{\"name\":\"wit-bindgen-core\",\"req\":\"^0.51.0\"},{\"name\":\"wit-component\",\"req\":\"^0.244.0\"}],\"features\":{\"clap\":[\"dep:clap\",\"wit-bindgen-core/clap\"],\"serde\":[\"dep:serde\",\"wit-bindgen-core/serde\"]}}", "wit-bindgen_0.51.0": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0\"},{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2.3.3\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.30\"},{\"name\":\"wit-bindgen-rust-macro\",\"optional\":true,\"req\":\"^0.51.0\"}],\"features\":{\"async\":[\"std\",\"wit-bindgen-rust-macro?/async\"],\"async-spawn\":[\"async\",\"dep:futures\"],\"bitflags\":[\"dep:bitflags\"],\"default\":[\"macros\",\"realloc\",\"async\",\"std\",\"bitflags\"],\"inter-task-wakeup\":[\"async\"],\"macros\":[\"dep:wit-bindgen-rust-macro\"],\"realloc\":[],\"rustc-dep-of-std\":[\"dep:core\",\"dep:alloc\"],\"std\":[]}}", - "wit-component_0.244.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"name\":\"bitflags\",\"req\":\"^2.3.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"req\":\"^2.7.0\"},{\"kind\":\"dev\",\"name\":\"libtest-mimic\",\"req\":\"^0.8.1\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"req\":\"^1.0.166\"},{\"name\":\"serde_derive\",\"req\":\"^1.0.166\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\",\"wasmparser\"],\"name\":\"wasm-encoder\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"name\":\"wasm-metadata\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"oci\"],\"kind\":\"dev\",\"name\":\"wasm-metadata\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"component-model\",\"simd\"],\"name\":\"wasmparser\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"component-model\",\"features\"],\"kind\":\"dev\",\"name\":\"wasmparser\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"component-model\"],\"kind\":\"dev\",\"name\":\"wasmprinter\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"cranelift\",\"component-model\",\"runtime\",\"gc-drc\"],\"kind\":\"dev\",\"name\":\"wasmtime\",\"req\":\"^34.0.1\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"default_features\":false,\"name\":\"wast\",\"optional\":true,\"req\":\"^244.0.0\"},{\"default_features\":false,\"name\":\"wat\",\"optional\":true,\"req\":\"^1.244.0\"},{\"default_features\":false,\"features\":[\"component-model\"],\"kind\":\"dev\",\"name\":\"wat\",\"req\":\"^1.244.0\"},{\"features\":[\"decoding\",\"serde\"],\"name\":\"wit-parser\",\"req\":\"^0.244.0\"}],\"features\":{\"dummy-module\":[\"dep:wat\"],\"semver-check\":[\"dummy-module\"],\"wat\":[\"dep:wast\",\"dep:wat\"]}}", - "wit-parser_0.244.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"id-arena\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2.7.0\"},{\"kind\":\"dev\",\"name\":\"libtest-mimic\",\"req\":\"^0.8.1\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"name\":\"semver\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"unicode-xid\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"validate\",\"component-model\",\"features\"],\"name\":\"wasmparser\",\"optional\":true,\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"component-model\"],\"name\":\"wat\",\"optional\":true,\"req\":\"^1.244.0\"}],\"features\":{\"decoding\":[\"dep:wasmparser\"],\"default\":[\"serde\",\"decoding\"],\"serde\":[\"dep:serde\",\"dep:serde_derive\",\"indexmap/serde\",\"serde_json\"],\"wat\":[\"decoding\",\"dep:wat\"]}}", "wl-clipboard-rs_0.9.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.168\"},{\"name\":\"log\",\"req\":\"^0.4.11\"},{\"features\":[\"io_safety\"],\"name\":\"os_pipe\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest-derive\",\"req\":\"^0.7\"},{\"features\":[\"fs\",\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tree_magic_mini\",\"req\":\"^3\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.11\"},{\"name\":\"wayland-client\",\"req\":\"^0.31.11\"},{\"features\":[\"client\",\"staging\"],\"name\":\"wayland-protocols\",\"req\":\"^0.32.9\"},{\"features\":[\"server\",\"staging\"],\"kind\":\"dev\",\"name\":\"wayland-protocols\",\"req\":\"^0.32.9\"},{\"features\":[\"client\"],\"name\":\"wayland-protocols-wlr\",\"req\":\"^0.3.9\"},{\"features\":[\"server\"],\"kind\":\"dev\",\"name\":\"wayland-protocols-wlr\",\"req\":\"^0.3.9\"},{\"kind\":\"dev\",\"name\":\"wayland-server\",\"req\":\"^0.31.10\"}],\"features\":{\"dlopen\":[\"native_lib\",\"wayland-backend/dlopen\",\"wayland-backend/dlopen\"],\"native_lib\":[\"wayland-backend/client_system\",\"wayland-backend/server_system\"]}}", "writeable_0.6.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"either\",\"optional\":true,\"req\":\"^1.9.0\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"either\":[\"dep:either\"]}}", "x11rb-protocol_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"all-extensions\":[\"composite\",\"damage\",\"dbe\",\"dpms\",\"dri2\",\"dri3\",\"glx\",\"present\",\"randr\",\"record\",\"render\",\"res\",\"screensaver\",\"shape\",\"shm\",\"sync\",\"xevie\",\"xf86dri\",\"xf86vidmode\",\"xfixes\",\"xinerama\",\"xinput\",\"xkb\",\"xprint\",\"xselinux\",\"xtest\",\"xv\",\"xvmc\"],\"composite\":[\"xfixes\"],\"damage\":[\"xfixes\"],\"dbe\":[],\"default\":[\"std\"],\"dpms\":[],\"dri2\":[],\"dri3\":[],\"extra-traits\":[],\"glx\":[],\"present\":[\"randr\",\"xfixes\",\"sync\",\"dri3\"],\"randr\":[\"render\"],\"record\":[],\"render\":[],\"request-parsing\":[],\"res\":[],\"resource_manager\":[\"std\"],\"screensaver\":[],\"shape\":[],\"shm\":[],\"std\":[],\"sync\":[],\"xevie\":[],\"xf86dri\":[],\"xf86vidmode\":[],\"xfixes\":[\"render\",\"shape\"],\"xinerama\":[],\"xinput\":[\"xfixes\"],\"xkb\":[],\"xprint\":[],\"xselinux\":[],\"xtest\":[],\"xv\":[\"shm\"],\"xvmc\":[\"xv\"]}}", @@ -1502,8 +1532,8 @@ "zbus_4.4.0": "{\"dependencies\":[{\"name\":\"async-broadcast\",\"req\":\"^0.7.0\"},{\"name\":\"async-executor\",\"optional\":true,\"req\":\"^1.11.0\"},{\"name\":\"async-fs\",\"optional\":true,\"req\":\"^2.1.2\"},{\"name\":\"async-io\",\"optional\":true,\"req\":\"^2.3.2\"},{\"name\":\"async-lock\",\"optional\":true,\"req\":\"^3.3.0\"},{\"name\":\"async-process\",\"req\":\"^2.2.2\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"async-recursion\",\"req\":\"^1.1.1\",\"target\":\"cfg(any(target_os = \\\"macos\\\", windows))\"},{\"name\":\"async-task\",\"optional\":true,\"req\":\"^4.7.1\"},{\"name\":\"async-trait\",\"req\":\"^0.1.80\"},{\"name\":\"blocking\",\"optional\":true,\"req\":\"^1.6.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"features\":[\"serde\"],\"name\":\"enumflags2\",\"req\":\"^0.7.9\"},{\"name\":\"event-listener\",\"req\":\"^5.3.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.30\"},{\"name\":\"futures-sink\",\"req\":\"^0.3.30\"},{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"req\":\"^0.3.30\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.30\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"default_features\":false,\"features\":[\"socket\",\"uio\",\"user\"],\"name\":\"nix\",\"req\":\"^0.29\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"ntest\",\"req\":\"^0.9.2\"},{\"name\":\"ordered-stream\",\"req\":\"^0.2\"},{\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.200\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"features\":[\"std\"],\"name\":\"sha1\",\"req\":\"^0.10.6\"},{\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"default_features\":false,\"features\":[\"trace\"],\"kind\":\"dev\",\"name\":\"test-log\",\"req\":\"^0.2.16\"},{\"features\":[\"rt\",\"net\",\"time\",\"fs\",\"io-util\",\"process\",\"sync\",\"tracing\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.37.0\"},{\"features\":[\"macros\",\"rt-multi-thread\",\"fs\",\"io-util\",\"net\",\"sync\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.37.0\"},{\"name\":\"tokio-vsock\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.18\"},{\"name\":\"uds_windows\",\"req\":\"^1.1.0\",\"target\":\"cfg(windows)\"},{\"name\":\"vsock\",\"optional\":true,\"req\":\"^0.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Authorization\",\"Win32_System_Memory\",\"Win32_Networking\",\"Win32_Networking_WinSock\",\"Win32_NetworkManagement\",\"Win32_NetworkManagement_IpHelper\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"},{\"name\":\"xdg-home\",\"req\":\"^1.1.0\"},{\"name\":\"zbus_macros\",\"req\":\"=4.4.0\"},{\"name\":\"zbus_names\",\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"zbus_xml\",\"req\":\"^4.0.0\"},{\"default_features\":false,\"features\":[\"enumflags2\"],\"name\":\"zvariant\",\"req\":\"^4.2.0\"}],\"features\":{\"async-io\":[\"dep:async-io\",\"async-executor\",\"async-task\",\"async-lock\",\"async-fs\",\"blocking\",\"futures-util/io\"],\"bus-impl\":[\"p2p\"],\"chrono\":[\"zvariant/chrono\"],\"default\":[\"async-io\"],\"heapless\":[\"zvariant/heapless\"],\"option-as-array\":[\"zvariant/option-as-array\"],\"p2p\":[],\"time\":[\"zvariant/time\"],\"tokio\":[\"dep:tokio\"],\"tokio-vsock\":[\"dep:tokio-vsock\",\"tokio\"],\"url\":[\"zvariant/url\"],\"uuid\":[\"zvariant/uuid\"],\"vsock\":[\"dep:vsock\",\"dep:async-io\"]}}", "zbus_macros_4.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.3.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.30\"},{\"name\":\"proc-macro-crate\",\"req\":\"^3.1.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.81\"},{\"name\":\"quote\",\"req\":\"^1.0.36\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.15\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.200\"},{\"features\":[\"extra-traits\",\"fold\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.64\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.93\"},{\"name\":\"zvariant_utils\",\"req\":\"=2.1.0\"}],\"features\":{}}", "zbus_names_3.0.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"enumflags2\"],\"name\":\"zvariant\",\"req\":\"^4.0.0\"}],\"features\":{}}", - "zerocopy-derive_0.8.39": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"dissimilar\",\"req\":\"^1.0.9\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"=0.2.17\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"=1.0.80\"},{\"name\":\"quote\",\"req\":\"^1.0.40\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"=1.0.40\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"visit\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"}],\"features\":{}}", - "zerocopy_0.8.39": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"elain\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"},{\"name\":\"zerocopy-derive\",\"req\":\"=0.8.39\",\"target\":\"cfg(any())\"},{\"name\":\"zerocopy-derive\",\"optional\":true,\"req\":\"=0.8.39\"},{\"kind\":\"dev\",\"name\":\"zerocopy-derive\",\"req\":\"=0.8.39\"}],\"features\":{\"__internal_use_only_features_that_work_on_stable\":[\"alloc\",\"derive\",\"simd\",\"std\"],\"alloc\":[],\"derive\":[\"zerocopy-derive\"],\"float-nightly\":[],\"simd\":[],\"simd-nightly\":[\"simd\"],\"std\":[\"alloc\"]}}", + "zerocopy-derive_0.8.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"dissimilar\",\"req\":\"^1.0.9\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"=0.2.17\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"=1.0.80\"},{\"name\":\"quote\",\"req\":\"^1.0.40\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"=1.0.40\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"visit\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"}],\"features\":{}}", + "zerocopy_0.8.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"elain\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"},{\"name\":\"zerocopy-derive\",\"req\":\"=0.8.37\",\"target\":\"cfg(any())\"},{\"name\":\"zerocopy-derive\",\"optional\":true,\"req\":\"=0.8.37\"},{\"kind\":\"dev\",\"name\":\"zerocopy-derive\",\"req\":\"=0.8.37\"}],\"features\":{\"__internal_use_only_features_that_work_on_stable\":[\"alloc\",\"derive\",\"simd\",\"std\"],\"alloc\":[],\"derive\":[\"zerocopy-derive\"],\"float-nightly\":[],\"simd\":[],\"simd-nightly\":[\"simd\"],\"std\":[\"alloc\"]}}", "zerofrom-derive_0.1.6": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"features\":[\"fold\"],\"name\":\"syn\",\"req\":\"^2.0.21\"},{\"name\":\"synstructure\",\"req\":\"^0.13.0\"}],\"features\":{}}", "zerofrom_0.1.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zerofrom-derive\",\"optional\":true,\"req\":\"^0.1.3\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"derive\":[\"dep:zerofrom-derive\"]}}", "zeroize_1.8.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"zeroize_derive\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"aarch64\":[],\"alloc\":[],\"default\":[\"alloc\"],\"derive\":[\"zeroize_derive\"],\"simd\":[],\"std\":[\"alloc\"]}}", @@ -1512,7 +1542,7 @@ "zerovec-derive_0.11.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.21\"}],\"features\":{}}", "zerovec_0.11.5": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"xxhash64\"],\"name\":\"twox-hash\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec-derive\",\"optional\":true,\"req\":\"^0.11.1\"}],\"features\":{\"alloc\":[\"serde?/alloc\"],\"databake\":[\"dep:databake\"],\"derive\":[\"dep:zerovec-derive\"],\"hashmap\":[\"dep:twox-hash\",\"alloc\"],\"serde\":[\"dep:serde\"],\"std\":[],\"yoke\":[\"dep:yoke\"]}}", "zip_2.4.2": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"req\":\"^1.4.1\",\"target\":\"cfg(fuzzing)\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crc32fast\",\"req\":\"^1.4\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"name\":\"deflate64\",\"optional\":true,\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"wasm_js\",\"std\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.1\"},{\"features\":[\"wasm_js\",\"std\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3.1\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"lzma-rs\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"memchr\",\"req\":\"^2.7\"},{\"default_features\":false,\"name\":\"nt-time\",\"optional\":true,\"req\":\"^0.10.6\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.15\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.37\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.37\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5\"},{\"name\":\"xz2\",\"optional\":true,\"req\":\"^0.1.7\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"},{\"name\":\"zopfli\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"_all-features\":[],\"_deflate-any\":[],\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\",\"getrandom\",\"zeroize\"],\"chrono\":[\"chrono/default\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate64\",\"deflate\",\"lzma\",\"time\",\"zstd\",\"xz\"],\"deflate\":[\"flate2/rust_backend\",\"deflate-zopfli\",\"deflate-flate2\"],\"deflate-flate2\":[\"_deflate-any\"],\"deflate-miniz\":[\"deflate\",\"deflate-flate2\"],\"deflate-zlib\":[\"flate2/zlib\",\"deflate-flate2\"],\"deflate-zlib-ng\":[\"flate2/zlib-ng\",\"deflate-flate2\"],\"deflate-zopfli\":[\"zopfli\",\"_deflate-any\"],\"lzma\":[\"lzma-rs/stream\"],\"nt-time\":[\"dep:nt-time\"],\"unreserved\":[],\"xz\":[\"dep:xz2\"]}}", - "zmij_1.0.21": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"opt-level\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"ryu\",\"req\":\"^1\"}],\"features\":{}}", + "zmij_1.0.19": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"opt-level\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"ryu\",\"req\":\"^1\"}],\"features\":{}}", "zopfli_0.8.3": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.19.0\"},{\"default_features\":false,\"name\":\"crc32fast\",\"optional\":true,\"req\":\"^1.5.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.8.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"proptest-derive\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"simd-adler32\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"default\":[\"gzip\",\"std\",\"zlib\"],\"gzip\":[\"dep:crc32fast\"],\"nightly\":[\"crc32fast?/nightly\"],\"std\":[\"crc32fast?/std\",\"dep:log\",\"simd-adler32?/std\"],\"zlib\":[\"dep:simd-adler32\"]}}", "zstd-safe_7.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.15\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"fat-lto\":[\"zstd-sys/fat-lto\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"seekable\":[\"zstd-sys/seekable\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"thin-lto\":[\"zstd-sys/thin-lto\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}", "zstd-sys_2.0.16+zstd.1.5.7": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.45\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.28\"}],\"features\":{\"debug\":[],\"default\":[\"legacy\",\"zdict_builder\",\"bindgen\"],\"experimental\":[],\"fat-lto\":[],\"legacy\":[],\"no_asm\":[],\"no_wasm_shim\":[],\"non-cargo\":[],\"pkg-config\":[],\"seekable\":[],\"std\":[],\"thin\":[],\"thin-lto\":[],\"zdict_builder\":[],\"zstdmt\":[]}}", diff --git a/codex-rs/.cargo/config.toml b/codex-rs/.cargo/config.toml index f5107cf73e..5d5eb8fd6f 100644 --- a/codex-rs/.cargo/config.toml +++ b/codex-rs/.cargo/config.toml @@ -1,5 +1,11 @@ [target.'cfg(all(windows, target_env = "msvc"))'] rustflags = ["-C", "link-arg=/STACK:8388608"] +# MSVC emits a warning about code that may trip "Cortex-A53 MPCore processor bug #843419" (see +# https://developer.arm.com/documentation/epm048406/latest) which is sometimes emitted by LLVM. +# Since Arm64 Windows 10+ isn't supported on that processor, it's safe to disable the warning. +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "link-arg=/STACK:8388608", "-C", "link-arg=/arm64hazardfree"] + [target.'cfg(all(windows, target_env = "gnu"))'] rustflags = ["-C", "link-arg=-Wl,--stack,8388608"] diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 91932b30f3..a68f99425c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -18,7 +18,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", @@ -31,15 +31,15 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.0" +version = "3.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "bitflags 2.11.0", + "bitflags 2.10.0", "bytes", "bytestring", "derive_more 2.1.1", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.13.0" +version = "4.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" dependencies = [ "actix-codec", "actix-http", @@ -282,7 +282,7 @@ checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -298,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "libc", ] @@ -396,9 +396,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "app_test_support" @@ -480,6 +480,58 @@ dependencies = [ "term", ] +[[package]] +name = "askama" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "syn 2.0.114", +] + +[[package]] +name = "askama_macros" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c" +dependencies = [ + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "unicode-ident", + "winnow", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -504,7 +556,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "synstructure", ] @@ -516,7 +568,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -576,9 +628,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.14.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -612,7 +664,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.4", + "rustix 1.1.3", "slab", "windows-sys 0.61.2", ] @@ -643,7 +695,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.4", + "rustix 1.1.3", ] [[package]] @@ -654,7 +706,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -669,7 +721,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.4", + "rustix 1.1.3", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -694,7 +746,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -711,7 +763,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -747,9 +799,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -889,7 +941,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -898,7 +950,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -924,9 +976,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ "serde_core", ] @@ -1007,9 +1059,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -1087,7 +1139,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -1122,9 +1174,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -1202,9 +1254,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -1244,9 +1296,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -1254,9 +1306,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ "anstream", "anstyle", @@ -1267,9 +1319,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.66" +version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", ] @@ -1283,7 +1335,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -1379,6 +1431,7 @@ dependencies = [ "codex-protocol", "codex-rmcp-client", "codex-shell-command", + "codex-state", "codex-utils-absolute-path", "codex-utils-cargo-bin", "codex-utils-cli", @@ -1398,7 +1451,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tokio-util", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", "tracing", "tracing-subscriber", "uuid", @@ -1564,7 +1617,7 @@ dependencies = [ "supports-color 3.0.2", "tempfile", "tokio", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", "tracing", ] @@ -1610,7 +1663,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", "tracing", ] @@ -1676,8 +1729,8 @@ dependencies = [ "sha2", "thiserror 2.0.18", "tokio", - "toml 0.9.12+spec-1.1.0", - "toml_edit 0.24.1+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", + "toml_edit 0.24.0+spec-1.1.0", "tracing", ] @@ -1687,6 +1740,7 @@ version = "0.0.0" dependencies = [ "anyhow", "arc-swap", + "askama", "assert_cmd", "assert_matches", "async-channel", @@ -1717,11 +1771,13 @@ dependencies = [ "codex-shell-escalation", "codex-skills", "codex-state", + "codex-test-macros", "codex-utils-absolute-path", "codex-utils-cargo-bin", "codex-utils-home-dir", "codex-utils-pty", "codex-utils-readiness", + "codex-utils-stream-parser", "codex-utils-string", "codex-windows-sandbox", "core-foundation 0.9.4", @@ -1735,6 +1791,7 @@ dependencies = [ "eventsource-stream", "futures", "http 1.4.0", + "iana-time-zone", "image", "indexmap 2.13.0", "insta", @@ -1771,8 +1828,8 @@ dependencies = [ "tokio", "tokio-tungstenite", "tokio-util", - "toml 0.9.12+spec-1.1.0", - "toml_edit 0.24.1+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", + "toml_edit 0.24.0+spec-1.1.0", "tracing", "tracing-subscriber", "tracing-test", @@ -1879,7 +1936,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -2040,6 +2097,7 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", + "chrono", "clap", "codex-utils-absolute-path", "codex-utils-home-dir", @@ -2092,6 +2150,7 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-string", "eventsource-stream", + "gethostname", "http 1.4.0", "opentelemetry", "opentelemetry-appender-tracing", @@ -2241,8 +2300,9 @@ dependencies = [ "anyhow", "async-trait", "clap", + "codex-protocol", + "codex-utils-absolute-path", "libc", - "path-absolutize", "pretty_assertions", "serde", "serde_json", @@ -2297,6 +2357,15 @@ dependencies = [ "uds_windows", ] +[[package]] +name = "codex-test-macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "codex-tui" version = "0.0.0" @@ -2332,6 +2401,7 @@ dependencies = [ "codex-utils-pty", "codex-utils-sandbox-summary", "codex-utils-sleep-inhibitor", + "codex-utils-string", "codex-windows-sandbox", "color-eyre", "cpal", @@ -2369,7 +2439,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", "tracing", "tracing-appender", "tracing-subscriber", @@ -2432,7 +2502,7 @@ dependencies = [ "codex-protocol", "pretty_assertions", "serde", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -2470,7 +2540,7 @@ version = "0.0.0" dependencies = [ "pretty_assertions", "serde_json", - "toml 0.9.12+spec-1.1.0", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -2536,6 +2606,13 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "codex-utils-stream-parser" +version = "0.0.0" +dependencies = [ + "pretty_assertions", +] + [[package]] name = "codex-utils-string" version = "0.0.0" @@ -2904,7 +2981,7 @@ name = "crossterm" version = "0.28.1" source = "git+https://github.com/nornagon/crossterm?branch=nornagon%2Fcolor-query#87db8bfa6dc99427fd3b071681b07fc31c6ce995" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "crossterm_winapi", "futures-core", "mio", @@ -3010,7 +3087,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3054,7 +3131,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3068,7 +3145,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3081,7 +3158,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3092,7 +3169,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3103,7 +3180,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3114,7 +3191,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3230,9 +3307,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -3257,7 +3334,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3287,7 +3364,7 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "unicode-xid", ] @@ -3301,7 +3378,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.114", "unicode-xid", ] @@ -3392,7 +3469,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", ] @@ -3414,7 +3491,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3467,7 +3544,7 @@ checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3487,9 +3564,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] @@ -3536,7 +3613,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3557,7 +3634,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3705,7 +3782,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -3715,7 +3792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.4", + "rustix 1.1.3", "windows-sys 0.59.0", ] @@ -3797,9 +3874,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "libz-sys", @@ -3941,9 +4018,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -3956,9 +4033,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -3966,15 +4043,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -3994,9 +4071,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -4013,32 +4090,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -4048,6 +4125,7 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] @@ -4091,7 +4169,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.3", "windows-link", ] @@ -4131,19 +4209,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - [[package]] name = "gif" version = "0.14.1" @@ -4176,7 +4241,7 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", ] [[package]] @@ -4488,7 +4553,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.5", ] [[package]] @@ -4522,13 +4587,14 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.4.0", "http-body", @@ -4594,7 +4660,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.114", "unic-langid", ] @@ -4608,7 +4674,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -4761,12 +4827,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -4908,7 +4968,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -4954,7 +5014,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -4978,9 +5038,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.22" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -5077,9 +5137,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.21" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -5090,13 +5150,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.21" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -5133,9 +5193,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -5153,7 +5213,7 @@ dependencies = [ "log", "secret-service", "security-framework 2.11.1", - "security-framework 3.7.0", + "security-framework 3.5.1", "windows-sys 0.60.2", "zbus", "zeroize", @@ -5236,12 +5296,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.182" @@ -5279,9 +5333,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "libc", - "redox_syscall 0.7.1", + "redox_syscall 0.7.0", ] [[package]] @@ -5318,7 +5372,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "libc", ] @@ -5330,9 +5384,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -5531,9 +5585,9 @@ checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -5635,17 +5689,17 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.18" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework 3.7.0", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -5656,7 +5710,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -5700,7 +5754,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -5712,7 +5766,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -5725,7 +5779,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -5762,7 +5816,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", @@ -5780,7 +5834,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", ] [[package]] @@ -5874,7 +5928,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -5947,7 +6001,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -5994,7 +6048,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -6006,7 +6060,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", "objc2-foundation", ] @@ -6027,7 +6081,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "dispatch2", "objc2", ] @@ -6038,7 +6092,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -6071,7 +6125,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -6089,7 +6143,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "block2", "libc", "objc2", @@ -6102,7 +6156,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", ] @@ -6113,7 +6167,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -6125,7 +6179,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "block2", "objc2", "objc2-cloud-kit", @@ -6213,7 +6267,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "libc", "once_cell", "onig_sys", @@ -6241,7 +6295,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -6258,9 +6312,15 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -6587,7 +6647,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -6655,11 +6715,11 @@ dependencies = [ [[package]] name = "png" -version = "0.18.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -6676,7 +6736,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.4", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -6761,9 +6821,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.4" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -6775,15 +6835,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.10" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.13" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -6799,16 +6859,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -6837,7 +6887,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -6865,16 +6915,16 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", "unarray", ] @@ -6898,14 +6948,14 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] name = "psl" -version = "2.1.193" +version = "2.1.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "530defb7f90261f0f83c9af4534efe2e2e7912b265c7890fd402e55f60791fd4" +checksum = "81dc6a90669f481b41cae3005c68efa36bef275b95aa9123a7af7f1c68c6e5b2" dependencies = [ "psl-types", ] @@ -6922,7 +6972,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -7101,7 +7151,7 @@ checksum = "453d60af031e23af2d48995e41b17023f6150044738680508b63671f8d7417dd" dependencies = [ "ahash", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.10.0", "chrono", "const_format", "csv", @@ -7234,7 +7284,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -7313,7 +7363,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "webpki-roots 1.0.6", + "webpki-roots 1.0.5", "x509-parser", ] @@ -7432,7 +7482,7 @@ name = "ratatui" version = "0.29.0" source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm", @@ -7496,16 +7546,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", ] [[package]] @@ -7547,7 +7597,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -7559,25 +7609,25 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", ] [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", ] [[package]] name = "regex-lite" -version = "0.1.9" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "regex-syntax" @@ -7587,9 +7637,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -7638,7 +7688,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.5", ] [[package]] @@ -7707,7 +7757,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -7755,7 +7805,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.117", + "syn 2.0.114", "walkdir", ] @@ -7821,7 +7871,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -7830,14 +7880,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -7863,10 +7913,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.7.0", + "security-framework 3.5.1", ] [[package]] @@ -7903,7 +7953,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "cfg-if", "clipboard-win", "fd-lock", @@ -7921,9 +7971,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "salsa20" @@ -8050,7 +8100,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8062,7 +8112,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8137,7 +8187,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -8146,11 +8196,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.7.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -8159,9 +8209,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.17.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -8190,11 +8240,10 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "sentry" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92d893ba7469d361a6958522fa440e4e2bc8bf4c5803cd1bf40b9af63f8f9a8" +checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f" dependencies = [ - "cfg_aliases 0.2.1", "httpdate", "native-tls", "reqwest", @@ -8211,9 +8260,9 @@ dependencies = [ [[package]] name = "sentry-actix" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56cb150fd6b55b3023714a3aaa1e3bdadfd44f164efc54fad69efc69aac36887" +checksum = "18bac0f6b8621fa0f85e298901e51161205788322e1a995e3764329020368058" dependencies = [ "actix-http", "actix-web", @@ -8224,9 +8273,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8784d0a27b5cd4b5f75769ffc84f0b7580e3c35e1af9cd83cb90b612d769cc" +checksum = "6cb1ef7534f583af20452b1b1bf610a60ed9c8dd2d8485e7bd064efc556a78fb" dependencies = [ "backtrace", "regex", @@ -8235,9 +8284,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5eb42f4cd4f9fdfec9e3b07b25a4c9769df83d218a7e846658984d5948ad3e" +checksum = "ebd6be899d9938390b6d1ec71e2f53bd9e57b6a9d8b1d5b049e5c364e7da9078" dependencies = [ "hostname", "libc", @@ -8249,9 +8298,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b1e7ca40f965db239da279bf278d87b7407469b98835f27f0c8e59ed189b06" +checksum = "26ab054c34b87f96c3e4701bea1888317cde30cc7e4a6136d2c48454ab96661c" dependencies = [ "rand 0.9.2", "sentry-types", @@ -8262,9 +8311,9 @@ dependencies = [ [[package]] name = "sentry-debug-images" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002561e49ea3a9de316e2efadc40fae553921b8ff41448f02ea85fd135a778d6" +checksum = "5637ec550dc6f8c49a711537950722d3fc4baa6fd433c371912104eaff31e2a5" dependencies = [ "findshlibs", "sentry-core", @@ -8272,9 +8321,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8906f8be87aea5ac7ef937323fb655d66607427f61007b99b7cb3504dc5a156c" +checksum = "3f02c7162f7b69b8de872b439d4696dc1d65f80b13ddd3c3831723def4756b63" dependencies = [ "sentry-backtrace", "sentry-core", @@ -8282,11 +8331,11 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b07eefe04486316c57aba08ab53dd44753c25102d1d3fe05775cc93a13262d9" +checksum = "e1dd47df349a80025819f3d25c3d2f751df705d49c65a4cdc0f130f700972a48" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "sentry-backtrace", "sentry-core", "tracing-core", @@ -8295,9 +8344,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.46.2" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567711f01f86a842057e1fc17779eba33a336004227e1a1e7e6cc2599e22e259" +checksum = "eecbd63e9d15a26a40675ed180d376fcb434635d2e33de1c24003f61e3e2230d" dependencies = [ "debugid", "hex", @@ -8337,7 +8386,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8348,7 +8397,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8397,7 +8446,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8449,7 +8498,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8467,9 +8516,9 @@ dependencies = [ [[package]] name = "serial2" -version = "0.2.34" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1401f562d358cdfdbdf8946e51a7871ede1db68bd0fd99bedc79e400241550" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" dependencies = [ "cfg-if", "libc", @@ -8478,9 +8527,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ "futures-executor", "futures-util", @@ -8493,13 +8542,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8758,7 +8807,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -8781,7 +8830,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.117", + "syn 2.0.114", "tokio", "url", ] @@ -8794,7 +8843,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -8839,7 +8888,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -8966,7 +9015,7 @@ dependencies = [ "dupe", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9088,7 +9137,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9100,7 +9149,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9141,9 +9190,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -9167,7 +9216,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9182,7 +9231,7 @@ dependencies = [ "once_cell", "onig", "plist", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", "serde", "serde_derive", "serde_json", @@ -9202,11 +9251,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.7.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -9229,14 +9278,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.3.4", "once_cell", - "rustix 1.1.4", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -9266,7 +9315,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.3", "windows-sys 0.60.2", ] @@ -9294,7 +9343,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9305,7 +9354,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "test-case-core", ] @@ -9328,7 +9377,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9377,7 +9426,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9388,7 +9437,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9532,7 +9581,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9619,9 +9668,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap 2.13.0", "serde_core", @@ -9655,9 +9704,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.24.1+spec-1.1.0" +version = "0.24.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" +checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" dependencies = [ "indexmap 2.13.0", "toml_datetime", @@ -9668,9 +9717,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -9683,9 +9732,9 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" dependencies = [ "async-trait", "base64 0.22.1", @@ -9711,9 +9760,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0" dependencies = [ "bytes", "prost", @@ -9745,7 +9794,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.4.0", @@ -9801,7 +9850,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9884,9 +9933,9 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" dependencies = [ "tracing-core", "tracing-subscriber", @@ -9895,12 +9944,12 @@ dependencies = [ [[package]] name = "tracing-test-macro" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -9911,7 +9960,7 @@ checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", - "regex-syntax 0.8.9", + "regex-syntax 0.8.8", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -9970,7 +10019,7 @@ checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "termcolor", ] @@ -10079,9 +10128,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -10169,9 +10218,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.2.0" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ "base64 0.22.1", "der", @@ -10235,11 +10284,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.3.4", "js-sys", "serde_core", "sha1_smol", @@ -10328,15 +10377,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasite" version = "0.1.0" @@ -10345,9 +10385,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -10358,9 +10398,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", "futures-util", @@ -10372,9 +10412,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10382,48 +10422,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -10437,18 +10455,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.12" @@ -10457,7 +10463,7 @@ checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.4", + "rustix 1.1.3", "smallvec", "wayland-sys", ] @@ -10468,8 +10474,8 @@ version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.11.0", - "rustix 1.1.4", + "bitflags 2.10.0", + "rustix 1.1.3", "wayland-backend", "wayland-scanner", ] @@ -10480,7 +10486,7 @@ version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -10492,7 +10498,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -10521,9 +10527,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -10541,9 +10547,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.1.0" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", "jni", @@ -10557,9 +10563,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" dependencies = [ "rustls-pki-types", ] @@ -10570,14 +10576,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -10595,7 +10601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", - "rustix 1.1.4", + "rustix 1.1.3", "winsafe", ] @@ -10757,7 +10763,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -10768,7 +10774,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -10779,7 +10785,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -10790,7 +10796,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -11249,88 +11255,6 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] [[package]] name = "wl-clipboard-rs" @@ -11341,7 +11265,7 @@ dependencies = [ "libc", "log", "os_pipe", - "rustix 1.1.4", + "rustix 1.1.3", "thiserror 2.0.18", "tree_magic_mini", "wayland-backend", @@ -11363,7 +11287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.4", + "rustix 1.1.3", "x11rb-protocol", ] @@ -11466,7 +11390,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "synstructure", ] @@ -11517,7 +11441,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "zvariant_utils", ] @@ -11534,22 +11458,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -11569,7 +11493,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "synstructure", ] @@ -11590,7 +11514,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -11624,7 +11548,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] [[package]] @@ -11659,9 +11583,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zopfli" @@ -11755,7 +11679,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", "zvariant_utils", ] @@ -11767,5 +11691,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.114", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index b3bb85d5aa..01836cde05 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -58,10 +58,12 @@ members = [ "utils/approval-presets", "utils/oss", "utils/fuzzy-match", + "utils/stream-parser", "codex-client", "codex-api", "state", "codex-experimental-api-macros", + "test-macros", ] resolver = "2" @@ -117,6 +119,7 @@ codex-shell-escalation = { path = "shell-escalation" } codex-skills = { path = "skills" } codex-state = { path = "state" } codex-stdio-to-uds = { path = "stdio-to-uds" } +codex-test-macros = { path = "test-macros" } codex-tui = { path = "tui" } codex-utils-absolute-path = { path = "utils/absolute-path" } codex-utils-approval-presets = { path = "utils/approval-presets" } @@ -134,6 +137,7 @@ codex-utils-readiness = { path = "utils/readiness" } codex-utils-rustls-provider = { path = "utils/rustls-provider" } codex-utils-sandbox-summary = { path = "utils/sandbox-summary" } codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" } +codex-utils-stream-parser = { path = "utils/stream-parser" } codex-utils-string = { path = "utils/string" } codex-windows-sandbox = { path = "windows-sandbox-rs" } core_test_support = { path = "core/tests/common" } @@ -145,6 +149,7 @@ allocative = "0.3.3" ansi-to-tui = "7.0.0" anyhow = "1" arboard = { version = "3", features = ["wayland-data-control"] } +askama = "0.15.4" assert_cmd = "2" assert_matches = "1.5.0" async-channel = "2.3.1" @@ -160,8 +165,8 @@ clap = "4" clap_complete = "4" color-eyre = "0.6.3" crossbeam-channel = "0.5.15" -csv = "1.3.1" crossterm = "0.28.1" +csv = "1.3.1" ctor = "0.6.3" derive_more = "2" diffy = "0.4.2" @@ -173,6 +178,7 @@ env-flags = "0.1.1" env_logger = "0.11.9" eventsource-stream = "0.2.3" futures = { version = "0.3", default-features = false } +gethostname = "1.1.0" globset = "0.4" http = "1.3.1" icu_decimal = "2.1" @@ -180,6 +186,7 @@ icu_locale_core = "2.1" icu_provider = { version = "2.1", features = ["sync"] } ignore = "0.4.23" image = { version = "^0.25.9", default-features = false } +iana-time-zone = "0.1.64" include_dir = "0.7.4" indexmap = "2.12.0" insta = "1.46.3" @@ -252,6 +259,7 @@ starlark = "0.13.0" strum = "0.27.2" strum_macros = "0.27.2" supports-color = "3.0.2" +syntect = "5" sys-locale = "0.3.2" tempfile = "3.23.0" test-log = "0.2.19" @@ -276,7 +284,6 @@ tracing-subscriber = "0.3.22" tracing-test = "0.2.5" tree-sitter = "0.25.10" tree-sitter-bash = "0.25" -syntect = "5" ts-rs = "11" tungstenite = { version = "0.27.0", features = ["deflate", "proxy"] } uds_windows = "1.1.0" @@ -346,6 +353,7 @@ ignored = [ [profile.release] lto = "fat" +split-debuginfo = "off" # Because we bundle some of these executables with the TypeScript CLI, we # remove everything to make the binary as small as possible. strip = "symbols" diff --git a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json index 37380f5988..84842fd194 100644 --- a/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ApplyPatchApprovalResponse.json @@ -59,7 +59,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index cee6e6d04b..03e42bed45 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -376,6 +376,70 @@ }, "type": "object" }, + "ExternalAgentConfigDetectParams": { + "properties": { + "cwds": { + "description": "Zero or more working directories to include for repo-scoped detection.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeHome": { + "description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + "type": "boolean" + } + }, + "type": "object" + }, + "ExternalAgentConfigImportParams": { + "properties": { + "migrationItems": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "migrationItems" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItem": { + "properties": { + "cwd": { + "description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + } + }, + "required": [ + "description", + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "MCP_SERVER_CONFIG" + ], + "type": "string" + }, "FeedbackUploadParams": { "properties": { "classification": { @@ -1276,7 +1340,7 @@ "type": "string" }, "output": { - "type": "string" + "$ref": "#/definitions/FunctionCallOutputPayload" }, "type": { "enum": [ @@ -1920,6 +1984,13 @@ "null" ] }, + "searchTerm": { + "description": "Optional substring filter for the extracted thread title.", + "type": [ + "string", + "null" + ] + }, "sortKey": { "anyOf": [ { @@ -1981,6 +2052,38 @@ ], "type": "object" }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "type": "object" + }, "ThreadResumeParams": { "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", "properties": { @@ -2190,6 +2293,12 @@ "type": "null" } ] + }, + "serviceName": { + "type": [ + "string", + "null" + ] } }, "type": "object" @@ -2205,6 +2314,17 @@ ], "type": "object" }, + "ThreadUnsubscribeParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "TurnInterruptParams": { "properties": { "threadId": { @@ -2696,6 +2816,30 @@ "title": "Thread/archiveRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/unsubscribe" + ], + "title": "Thread/unsubscribeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnsubscribeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/unsubscribeRequest", + "type": "object" + }, { "properties": { "id": { @@ -3390,6 +3534,54 @@ "title": "Config/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/detect" + ], + "title": "ExternalAgentConfig/detectRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigDetectParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/detectRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/import" + ], + "title": "ExternalAgentConfig/importRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/importRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index b7ad3fe315..5a19a39281 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -1,6 +1,101 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "read": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalMacOsPermissions": { + "properties": { + "accessibility": { + "type": [ + "boolean", + "null" + ] + }, + "automations": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsAutomationValue" + }, + { + "type": "null" + } + ] + }, + "calendar": { + "type": [ + "boolean", + "null" + ] + }, + "preferences": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPreferencesValue" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AdditionalPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalMacOsPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "CommandAction": { "oneOf": [ { @@ -111,6 +206,108 @@ } ] }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, + "MacOsAutomationValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "MacOsPreferencesValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "NetworkApprovalContext": { "properties": { "host": { @@ -134,6 +331,28 @@ "socks5Udp" ], "type": "string" + }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": [ + "action", + "host" + ], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": [ + "allow", + "deny" + ], + "type": "string" } }, "properties": { @@ -180,7 +399,7 @@ "type": "null" } ], - "description": "Optional context for managed-network approval prompts." + "description": "Optional context for a managed-network approval prompt." }, "proposedExecpolicyAmendment": { "description": "Optional proposed execpolicy amendment to allow similar commands without prompting.", @@ -192,6 +411,16 @@ "null" ] }, + "proposedNetworkPolicyAmendments": { + "description": "Optional proposed network policy amendments (allow/deny host) for future requests.", + "items": { + "$ref": "#/definitions/NetworkPolicyAmendment" + }, + "type": [ + "array", + "null" + ] + }, "reason": { "description": "Optional explanatory reason (e.g. request for network access).", "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json index fcc3eba786..0b7986fba9 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json @@ -11,7 +11,7 @@ "type": "string" }, { - "description": "User approved the command and future identical commands should run without prompting.", + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ "acceptForSession" ], @@ -42,6 +42,28 @@ "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", "type": "object" }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User denied the command. The agent will continue the turn.", "enum": [ @@ -57,6 +79,28 @@ "type": "string" } ] + }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": [ + "action", + "host" + ], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": [ + "allow", + "deny" + ], + "type": "string" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index c81f69d109..b3e4317065 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -5,23 +5,6 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, - "AdditionalPermissions": { - "properties": { - "fs_read": { - "items": { - "type": "string" - }, - "type": "array" - }, - "fs_write": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, "AgentMessageContent": { "oneOf": [ { @@ -517,6 +500,50 @@ ], "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -1633,7 +1660,7 @@ "additional_permissions": { "anyOf": [ { - "$ref": "#/definitions/AdditionalPermissions" + "$ref": "#/definitions/PermissionProfile" }, { "type": "null" @@ -1648,6 +1675,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -1795,26 +1832,66 @@ }, { "properties": { - "item_id": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", "type": "string" }, - "skill_name": { + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", "type": "string" }, "type": { "enum": [ - "skill_request_approval" + "dynamic_tool_call_response" ], - "title": "SkillRequestApprovalEventMsgType", + "title": "DynamicToolCallResponseEventMsgType", "type": "string" } }, "required": [ - "item_id", - "skill_name", + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", "type" ], - "title": "SkillRequestApprovalEventMsg", + "title": "DynamicToolCallResponseEventMsg", "type": "object" }, { @@ -3330,6 +3407,29 @@ } ] }, + "FileSystemPermissions": { + "properties": { + "read": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -3523,6 +3623,66 @@ ], "type": "string" }, + "MacOsAutomationValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "MacOsPermissions": { + "properties": { + "accessibility": { + "type": [ + "boolean", + "null" + ] + }, + "automations": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsAutomationValue" + }, + { + "type": "null" + } + ] + }, + "calendar": { + "type": [ + "boolean", + "null" + ] + }, + "preferences": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPreferencesValue" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "MacOsPreferencesValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "McpAuthStatus": { "enum": [ "unsupported", @@ -3840,6 +4000,37 @@ ], "type": "string" }, + "PermissionProfile": { + "properties": { + "file_system": { + "anyOf": [ + { + "$ref": "#/definitions/FileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "PlanItemArg": { "additionalProperties": false, "properties": { @@ -4631,7 +4822,7 @@ "type": "string" }, "output": { - "type": "string" + "$ref": "#/definitions/FunctionCallOutputPayload" }, "type": { "enum": [ @@ -4790,6 +4981,86 @@ ], "type": "object" }, + "ReviewDecision": { + "description": "User's decision in response to an ExecApprovalRequest.", + "oneOf": [ + { + "description": "User has approved this command and the agent should execute it.", + "enum": [ + "approved" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.", + "properties": { + "approved_execpolicy_amendment": { + "properties": { + "proposed_execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "proposed_execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "approved_execpolicy_amendment" + ], + "title": "ApprovedExecpolicyAmendmentReviewDecision", + "type": "object" + }, + { + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", + "enum": [ + "approved_for_session" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.", + "properties": { + "network_policy_amendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "network_policy_amendment" + ], + "title": "NetworkPolicyAmendmentReviewDecision", + "type": "object" + }, + { + "description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.", + "enum": [ + "denied" + ], + "type": "string" + }, + { + "description": "User has denied this command and the agent should not do anything until the user's next command.", + "enum": [ + "abort" + ], + "type": "string" + } + ] + }, "ReviewFinding": { "description": "A single review finding describing an observed issue or recommendation.", "properties": { @@ -6945,7 +7216,7 @@ "additional_permissions": { "anyOf": [ { - "$ref": "#/definitions/AdditionalPermissions" + "$ref": "#/definitions/PermissionProfile" }, { "type": "null" @@ -6960,6 +7231,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -7107,26 +7388,66 @@ }, { "properties": { - "item_id": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", "type": "string" }, - "skill_name": { + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", "type": "string" }, "type": { "enum": [ - "skill_request_approval" + "dynamic_tool_call_response" ], - "title": "SkillRequestApprovalEventMsgType", + "title": "DynamicToolCallResponseEventMsgType", "type": "string" } }, "required": [ - "item_id", - "skill_name", + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", "type" ], - "title": "SkillRequestApprovalEventMsg", + "title": "DynamicToolCallResponseEventMsg", "type": "object" }, { diff --git a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json index ac15f23658..baedafa403 100644 --- a/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json @@ -59,7 +59,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index b2dc4c3c3e..eca77d2bfa 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -778,6 +778,58 @@ ], "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "ErrorNotification": { "properties": { "error": { @@ -1674,6 +1726,17 @@ ], "type": "object" }, + "ThreadClosedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadId": { "type": "string" }, @@ -1965,6 +2028,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { @@ -2177,6 +2293,120 @@ ], "type": "object" }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "type": "object" + }, + "ThreadRealtimeClosedNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime transport closes.", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ThreadRealtimeErrorNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime encounters an error.", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId" + ], + "type": "object" + }, + "ThreadRealtimeItemAddedNotification": { + "description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.", + "properties": { + "item": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "type": "object" + }, + "ThreadRealtimeOutputAudioDeltaNotification": { + "description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.", + "properties": { + "audio": { + "$ref": "#/definitions/ThreadRealtimeAudioChunk" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "audio", + "threadId" + ], + "type": "object" + }, + "ThreadRealtimeStartedNotification": { + "description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.", + "properties": { + "sessionId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadStartedNotification": { "properties": { "thread": { @@ -2911,6 +3141,26 @@ "title": "Thread/unarchivedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/closed" + ], + "title": "Thread/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/closedNotification", + "type": "object" + }, { "properties": { "method": { @@ -3453,6 +3703,106 @@ "title": "FuzzyFileSearch/sessionCompletedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/started" + ], + "title": "Thread/realtime/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/itemAdded" + ], + "title": "Thread/realtime/itemAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemAddedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/itemAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/outputAudio/delta" + ], + "title": "Thread/realtime/outputAudio/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/outputAudio/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/error" + ], + "title": "Thread/realtime/errorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/errorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/closed" + ], + "title": "Thread/realtime/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/closedNotification", + "type": "object" + }, { "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index fb73dc0edd..9138440548 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -1,6 +1,101 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "read": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalMacOsPermissions": { + "properties": { + "accessibility": { + "type": [ + "boolean", + "null" + ] + }, + "automations": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsAutomationValue" + }, + { + "type": "null" + } + ] + }, + "calendar": { + "type": [ + "boolean", + "null" + ] + }, + "preferences": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPreferencesValue" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AdditionalPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalMacOsPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "ApplyPatchApprovalParams": { "properties": { "callId": { @@ -177,6 +272,85 @@ } ] }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, "CommandExecutionRequestApprovalParams": { "properties": { "approvalId": { @@ -222,7 +396,7 @@ "type": "null" } ], - "description": "Optional context for managed-network approval prompts." + "description": "Optional context for a managed-network approval prompt." }, "proposedExecpolicyAmendment": { "description": "Optional proposed execpolicy amendment to allow similar commands without prompting.", @@ -234,6 +408,16 @@ "null" ] }, + "proposedNetworkPolicyAmendments": { + "description": "Optional proposed network policy amendments (allow/deny host) for future requests.", + "items": { + "$ref": "#/definitions/NetworkPolicyAmendment" + }, + "type": [ + "array", + "null" + ] + }, "reason": { "description": "Optional explanatory reason (e.g. request for network access).", "type": [ @@ -430,6 +614,29 @@ ], "type": "object" }, + "MacOsAutomationValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "MacOsPreferencesValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "NetworkApprovalContext": { "properties": { "host": { @@ -454,6 +661,28 @@ ], "type": "string" }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": [ + "action", + "host" + ], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, "ParsedCommand": { "oneOf": [ { @@ -576,21 +805,6 @@ } ] }, - "SkillRequestApprovalParams": { - "properties": { - "itemId": { - "type": "string" - }, - "skillName": { - "type": "string" - } - }, - "required": [ - "itemId", - "skillName" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -752,30 +966,6 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "skill/requestApproval" - ], - "title": "Skill/requestApprovalRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/SkillRequestApprovalParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Skill/requestApprovalRequest", - "type": "object" - }, { "description": "Execute a dynamic tool call on the client.", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json deleted file mode 100644 index 0bea163ccb..0000000000 --- a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalResponse.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "SkillApprovalDecision": { - "enum": [ - "approve", - "decline" - ], - "type": "string" - } - }, - "properties": { - "decision": { - "$ref": "#/definitions/SkillApprovalDecision" - } - }, - "required": [ - "decision" - ], - "title": "SkillRequestApprovalResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 859970ef66..a0863ad61d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1,19 +1,93 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "AdditionalPermissions": { + "AdditionalFileSystemPermissions": { "properties": { - "fs_read": { + "read": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, - "type": "array" + "type": [ + "array", + "null" + ] }, - "fs_write": { + "write": { "items": { - "type": "string" + "$ref": "#/definitions/AbsolutePathBuf" }, - "type": "array" + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalMacOsPermissions": { + "properties": { + "accessibility": { + "type": [ + "boolean", + "null" + ] + }, + "automations": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsAutomationValue" + }, + { + "type": "null" + } + ] + }, + "calendar": { + "type": [ + "boolean", + "null" + ] + }, + "preferences": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPreferencesValue" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AdditionalPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalMacOsPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -401,6 +475,30 @@ "title": "Thread/archiveRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/unsubscribe" + ], + "title": "Thread/unsubscribeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUnsubscribeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/unsubscribeRequest", + "type": "object" + }, { "properties": { "id": { @@ -1095,6 +1193,54 @@ "title": "Config/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/detect" + ], + "title": "ExternalAgentConfig/detectRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ExternalAgentConfigDetectParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/detectRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/import" + ], + "title": "ExternalAgentConfig/importRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ExternalAgentConfigImportParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/importRequest", + "type": "object" + }, { "properties": { "id": { @@ -1296,7 +1442,7 @@ "type": "string" }, { - "description": "User approved the command and future identical commands should run without prompting.", + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", "enum": [ "acceptForSession" ], @@ -1327,6 +1473,28 @@ "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", "type": "object" }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, { "description": "User denied the command. The agent will continue the turn.", "enum": [ @@ -1389,7 +1557,7 @@ "type": "null" } ], - "description": "Optional context for managed-network approval prompts." + "description": "Optional context for a managed-network approval prompt." }, "proposedExecpolicyAmendment": { "description": "Optional proposed execpolicy amendment to allow similar commands without prompting.", @@ -1401,6 +1569,16 @@ "null" ] }, + "proposedNetworkPolicyAmendments": { + "description": "Optional proposed network policy amendments (allow/deny host) for future requests.", + "items": { + "$ref": "#/definitions/NetworkPolicyAmendment" + }, + "type": [ + "array", + "null" + ] + }, "reason": { "description": "Optional explanatory reason (e.g. request for network access).", "type": [ @@ -1486,50 +1664,6 @@ ], "type": "object" }, - "DynamicToolCallOutputContentItem": { - "oneOf": [ - { - "properties": { - "text": { - "type": "string" - }, - "type": { - "enum": [ - "inputText" - ], - "title": "InputTextDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "title": "InputTextDynamicToolCallOutputContentItem", - "type": "object" - }, - { - "properties": { - "imageUrl": { - "type": "string" - }, - "type": { - "enum": [ - "inputImage" - ], - "title": "InputImageDynamicToolCallOutputContentItemType", - "type": "string" - } - }, - "required": [ - "imageUrl", - "type" - ], - "title": "InputImageDynamicToolCallOutputContentItem", - "type": "object" - } - ] - }, "DynamicToolCallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -1562,7 +1696,7 @@ "properties": { "contentItems": { "items": { - "$ref": "#/definitions/DynamicToolCallOutputContentItem" + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" }, "type": "array" }, @@ -2694,7 +2828,7 @@ "additional_permissions": { "anyOf": [ { - "$ref": "#/definitions/AdditionalPermissions" + "$ref": "#/definitions/PermissionProfile" }, { "type": "null" @@ -2709,6 +2843,16 @@ "null" ] }, + "available_decisions": { + "description": "Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + "items": { + "$ref": "#/definitions/ReviewDecision" + }, + "type": [ + "array", + "null" + ] + }, "call_id": { "description": "Identifier for the associated command execution item.", "type": "string" @@ -2856,26 +3000,66 @@ }, { "properties": { - "item_id": { + "arguments": { + "description": "Dynamic tool call arguments." + }, + "call_id": { + "description": "Identifier for the corresponding DynamicToolCallRequest.", "type": "string" }, - "skill_name": { + "content_items": { + "description": "Dynamic tool response content items.", + "items": { + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" + }, + "type": "array" + }, + "duration": { + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ], + "description": "The duration of the dynamic tool call." + }, + "error": { + "description": "Optional error text when the tool call failed before producing a response.", + "type": [ + "string", + "null" + ] + }, + "success": { + "description": "Whether the tool call succeeded.", + "type": "boolean" + }, + "tool": { + "description": "Dynamic tool name.", + "type": "string" + }, + "turn_id": { + "description": "Turn ID that this dynamic tool call belongs to.", "type": "string" }, "type": { "enum": [ - "skill_request_approval" + "dynamic_tool_call_response" ], - "title": "SkillRequestApprovalEventMsgType", + "title": "DynamicToolCallResponseEventMsgType", "type": "string" } }, "required": [ - "item_id", - "skill_name", + "arguments", + "call_id", + "content_items", + "duration", + "success", + "tool", + "turn_id", "type" ], - "title": "SkillRequestApprovalEventMsg", + "title": "DynamicToolCallResponseEventMsg", "type": "object" }, { @@ -4534,6 +4718,29 @@ "title": "FileChangeRequestApprovalResponse", "type": "object" }, + "FileSystemPermissions": { + "properties": { + "read": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, "FuzzyFileSearchParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -4834,6 +5041,66 @@ "title": "JSONRPCResponse", "type": "object" }, + "MacOsAutomationValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "MacOsPermissions": { + "properties": { + "accessibility": { + "type": [ + "boolean", + "null" + ] + }, + "automations": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsAutomationValue" + }, + { + "type": "null" + } + ] + }, + "calendar": { + "type": [ + "boolean", + "null" + ] + }, + "preferences": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPreferencesValue" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "MacOsPreferencesValue": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, "McpInvocation": { "properties": { "arguments": { @@ -5093,6 +5360,37 @@ } ] }, + "PermissionProfile": { + "properties": { + "file_system": { + "anyOf": [ + { + "$ref": "#/definitions/FileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/MacOsPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, "PlanItemArg": { "additionalProperties": false, "properties": { @@ -5388,7 +5686,7 @@ "type": "object" }, { - "description": "User has approved this command and wants to automatically approve any future identical instances (`command` and `cwd` match exactly) for the remainder of the session.", + "description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.", "enum": [ "approved_for_session" ], @@ -5615,6 +5913,26 @@ "title": "Thread/unarchivedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/closed" + ], + "title": "Thread/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/closedNotification", + "type": "object" + }, { "properties": { "method": { @@ -6157,6 +6475,106 @@ "title": "FuzzyFileSearch/sessionCompletedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/started" + ], + "title": "Thread/realtime/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/itemAdded" + ], + "title": "Thread/realtime/itemAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeItemAddedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/itemAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/outputAudio/delta" + ], + "title": "Thread/realtime/outputAudio/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeOutputAudioDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/outputAudio/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/error" + ], + "title": "Thread/realtime/errorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/errorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/closed" + ], + "title": "Thread/realtime/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/closedNotification", + "type": "object" + }, { "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", "properties": { @@ -6300,30 +6718,6 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "skill/requestApproval" - ], - "title": "Skill/requestApprovalRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/SkillRequestApprovalParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Skill/requestApprovalRequest", - "type": "object" - }, { "description": "Execute a dynamic tool call on the client.", "properties": { @@ -6445,43 +6839,6 @@ ], "type": "object" }, - "SkillApprovalDecision": { - "enum": [ - "approve", - "decline" - ], - "type": "string" - }, - "SkillRequestApprovalParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "itemId": { - "type": "string" - }, - "skillName": { - "type": "string" - } - }, - "required": [ - "itemId", - "skillName" - ], - "title": "SkillRequestApprovalParams", - "type": "object" - }, - "SkillRequestApprovalResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "properties": { - "decision": { - "$ref": "#/definitions/SkillApprovalDecision" - } - }, - "required": [ - "decision" - ], - "title": "SkillRequestApprovalResponse", - "type": "object" - }, "StepStatus": { "enum": [ "pending", @@ -7718,14 +8075,8 @@ "type": "object" }, "CollaborationModeMask": { - "description": "A mask for collaboration mode settings, allowing partial updates. All fields except `name` are optional, enabling selective updates.", + "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", "properties": { - "developer_instructions": { - "type": [ - "string", - "null" - ] - }, "mode": { "anyOf": [ { @@ -8726,6 +9077,58 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "DynamicToolSpec": { "properties": { "description": { @@ -8905,6 +9308,95 @@ } ] }, + "ExternalAgentConfigDetectParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "Zero or more working directories to include for repo-scoped detection.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeHome": { + "description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + "type": "boolean" + } + }, + "title": "ExternalAgentConfigDetectParams", + "type": "object" + }, + "ExternalAgentConfigDetectResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "items": { + "items": { + "$ref": "#/definitions/v2/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "title": "ExternalAgentConfigDetectResponse", + "type": "object" + }, + "ExternalAgentConfigImportParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "migrationItems": { + "items": { + "$ref": "#/definitions/v2/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "migrationItems" + ], + "title": "ExternalAgentConfigImportParams", + "type": "object" + }, + "ExternalAgentConfigImportResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExternalAgentConfigImportResponse", + "type": "object" + }, + "ExternalAgentConfigMigrationItem": { + "properties": { + "cwd": { + "description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/v2/ExternalAgentConfigMigrationItemType" + } + }, + "required": [ + "description", + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "MCP_SERVER_CONFIG" + ], + "type": "string" + }, "FeedbackUploadParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -9733,6 +10225,16 @@ }, "Model": { "properties": { + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, "defaultReasoningEffort": { "$ref": "#/definitions/v2/ReasoningEffort" }, @@ -9779,6 +10281,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -9793,6 +10305,17 @@ ], "type": "object" }, + "ModelAvailabilityNux": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, "ModelListParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -9881,6 +10404,35 @@ "title": "ModelReroutedNotification", "type": "object" }, + "ModelUpgradeInfo": { + "properties": { + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, "NetworkAccess": { "enum": [ "restricted", @@ -10890,7 +11442,7 @@ "type": "string" }, "output": { - "type": "string" + "$ref": "#/definitions/v2/FunctionCallOutputPayload" }, "type": { "enum": [ @@ -12014,6 +12566,19 @@ "title": "ThreadArchivedNotification", "type": "object" }, + "ThreadClosedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadClosedNotification", + "type": "object" + }, "ThreadCompactStartParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12438,6 +13003,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/v2/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { @@ -12676,6 +13294,13 @@ "null" ] }, + "searchTerm": { + "description": "Optional substring filter for the extracted thread title.", + "type": [ + "string", + "null" + ] + }, "sortKey": { "anyOf": [ { @@ -12821,6 +13446,130 @@ "title": "ThreadReadResponse", "type": "object" }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "type": "object" + }, + "ThreadRealtimeClosedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime transport closes.", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRealtimeClosedNotification", + "type": "object" + }, + "ThreadRealtimeErrorNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime encounters an error.", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId" + ], + "title": "ThreadRealtimeErrorNotification", + "type": "object" + }, + "ThreadRealtimeItemAddedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.", + "properties": { + "item": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "title": "ThreadRealtimeItemAddedNotification", + "type": "object" + }, + "ThreadRealtimeOutputAudioDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.", + "properties": { + "audio": { + "$ref": "#/definitions/v2/ThreadRealtimeAudioChunk" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "audio", + "threadId" + ], + "title": "ThreadRealtimeOutputAudioDeltaNotification", + "type": "object" + }, + "ThreadRealtimeStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.", + "properties": { + "sessionId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRealtimeStartedNotification", + "type": "object" + }, "ThreadResumeParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", @@ -13103,6 +13852,12 @@ "type": "null" } ] + }, + "serviceName": { + "type": [ + "string", + "null" + ] } }, "title": "ThreadStartParams", @@ -13338,6 +14093,40 @@ "title": "ThreadUnarchivedNotification", "type": "object" }, + "ThreadUnsubscribeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnsubscribeParams", + "type": "object" + }, + "ThreadUnsubscribeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/v2/ThreadUnsubscribeStatus" + } + }, + "required": [ + "status" + ], + "title": "ThreadUnsubscribeResponse", + "type": "object" + }, + "ThreadUnsubscribeStatus": { + "enum": [ + "notLoaded", + "notSubscribed", + "unsubscribed" + ], + "type": "string" + }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectParams.json b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectParams.json new file mode 100644 index 0000000000..20ddd6e48a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectParams.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "Zero or more working directories to include for repo-scoped detection.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeHome": { + "description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + "type": "boolean" + } + }, + "title": "ExternalAgentConfigDetectParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectResponse.json new file mode 100644 index 0000000000..a73e515c2b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigDetectResponse.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ExternalAgentConfigMigrationItem": { + "properties": { + "cwd": { + "description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + } + }, + "required": [ + "description", + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "MCP_SERVER_CONFIG" + ], + "type": "string" + } + }, + "properties": { + "items": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "title": "ExternalAgentConfigDetectResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportParams.json b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportParams.json new file mode 100644 index 0000000000..85af249590 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportParams.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ExternalAgentConfigMigrationItem": { + "properties": { + "cwd": { + "description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + } + }, + "required": [ + "description", + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "MCP_SERVER_CONFIG" + ], + "type": "string" + } + }, + "properties": { + "migrationItems": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "migrationItems" + ], + "title": "ExternalAgentConfigImportParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportResponse.json new file mode 100644 index 0000000000..6823495d3c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ExternalAgentConfigImportResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExternalAgentConfigImportResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 98d8a631e9..c8824f560e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -185,6 +185,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -633,6 +685,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index ee10e470e0..04dbc07389 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -185,6 +185,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -633,6 +685,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json index 2afa018dc5..4023f76b51 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -22,6 +22,16 @@ }, "Model": { "properties": { + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, "defaultReasoningEffort": { "$ref": "#/definitions/ReasoningEffort" }, @@ -68,6 +78,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -82,6 +102,46 @@ ], "type": "object" }, + "ModelAvailabilityNux": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "ModelUpgradeInfo": { + "properties": { + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index 748eeaab46..4717ff266b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -565,7 +565,7 @@ "type": "string" }, "output": { - "type": "string" + "$ref": "#/definitions/FunctionCallOutputPayload" }, "type": { "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 760477095f..61449aa785 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadClosedNotification.json similarity index 52% rename from codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json rename to codex-rs/app-server-protocol/schema/json/v2/ThreadClosedNotification.json index eebe6570b2..13e7f57748 100644 --- a/codex-rs/app-server-protocol/schema/json/SkillRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadClosedNotification.json @@ -1,17 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "itemId": { - "type": "string" - }, - "skillName": { + "threadId": { "type": "string" } }, "required": [ - "itemId", - "skillName" + "threadId" ], - "title": "SkillRequestApprovalParams", + "title": "ThreadClosedNotification", "type": "object" } \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 71f9da2f3b..c1dee7a589 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListParams.json index fa88ea3f7c..c5cf1364c9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListParams.json @@ -65,6 +65,13 @@ "null" ] }, + "searchTerm": { + "description": "Optional substring filter for the extracted thread title.", + "type": [ + "string", + "null" + ] + }, "sortKey": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 5907d13cea..e9bce99fbd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 9944457c3e..e487ad0d8d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeClosedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeClosedNotification.json new file mode 100644 index 0000000000..edfba83b7b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeClosedNotification.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime transport closes.", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRealtimeClosedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeErrorNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeErrorNotification.json new file mode 100644 index 0000000000..e7ec760302 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeErrorNotification.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime encounters an error.", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId" + ], + "title": "ThreadRealtimeErrorNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeItemAddedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeItemAddedNotification.json new file mode 100644 index 0000000000..06de7e00e7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeItemAddedNotification.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.", + "properties": { + "item": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "title": "ThreadRealtimeItemAddedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeOutputAudioDeltaNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeOutputAudioDeltaNotification.json new file mode 100644 index 0000000000..d4df6194fa --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeOutputAudioDeltaNotification.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "type": "object" + } + }, + "description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.", + "properties": { + "audio": { + "$ref": "#/definitions/ThreadRealtimeAudioChunk" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "audio", + "threadId" + ], + "title": "ThreadRealtimeOutputAudioDeltaNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeStartedNotification.json new file mode 100644 index 0000000000..1584112640 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeStartedNotification.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.", + "properties": { + "sessionId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRealtimeStartedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 29d6fbc6d7..ef7607d3ca 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -615,7 +615,7 @@ "type": "string" }, "output": { - "type": "string" + "$ref": "#/definitions/FunctionCallOutputPayload" }, "type": { "enum": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 7778773a16..b606e34a28 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index c4bcdf7092..faea619708 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 6b9c1568f0..5f2dda11a1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -150,6 +150,12 @@ "type": "null" } ] + }, + "serviceName": { + "type": [ + "string", + "null" + ] } }, "title": "ThreadStartParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 6b3d2ebc58..a1de9f9efa 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -345,6 +345,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -1207,6 +1259,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 2fbd9538f3..f8faff0101 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index df3e7beee5..b1be7b2de6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -980,6 +1032,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeParams.json new file mode 100644 index 0000000000..dec3670cc2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnsubscribeParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeResponse.json new file mode 100644 index 0000000000..2e545dbf9f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnsubscribeResponse.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ThreadUnsubscribeStatus": { + "enum": [ + "notLoaded", + "notSubscribed", + "unsubscribed" + ], + "type": "string" + } + }, + "properties": { + "status": { + "$ref": "#/definitions/ThreadUnsubscribeStatus" + } + }, + "required": [ + "status" + ], + "title": "ThreadUnsubscribeResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 2c8eec7f90..84bb60c8e8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index 5585cd455a..863814e1b4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 4462c314d2..aba5d973ab 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -299,6 +299,58 @@ ], "type": "string" }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, "FileUpdateChange": { "properties": { "diff": { @@ -747,6 +799,59 @@ "title": "McpToolCallThreadItem", "type": "object" }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, { "properties": { "agentsStates": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 8344592871..9707644002 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -28,6 +28,8 @@ import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; import type { ConfigReadParams } from "./v2/ConfigReadParams"; import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams"; +import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams"; +import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams"; import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; @@ -50,6 +52,7 @@ import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams"; import type { ThreadStartParams } from "./v2/ThreadStartParams"; import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; +import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams"; import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; import type { TurnStartParams } from "./v2/TurnStartParams"; import type { TurnSteerParams } from "./v2/TurnSteerParams"; @@ -58,4 +61,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts b/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts deleted file mode 100644 index 05902676d7..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/CollaborationModeMask.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ModeKind } from "./ModeKind"; -import type { ReasoningEffort } from "./ReasoningEffort"; - -/** - * A mask for collaboration mode settings, allowing partial updates. - * All fields except `name` are optional, enabling selective updates. - */ -export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, developer_instructions: string | null | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.ts b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.ts new file mode 100644 index 0000000000..8f432109d1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallOutputContentItem.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DynamicToolCallOutputContentItem = { "type": "inputText", text: string, } | { "type": "inputImage", imageUrl: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts new file mode 100644 index 0000000000..442c0ce6f3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/DynamicToolCallResponseEvent.ts @@ -0,0 +1,39 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +import type { JsonValue } from "./serde_json/JsonValue"; + +export type DynamicToolCallResponseEvent = { +/** + * Identifier for the corresponding DynamicToolCallRequest. + */ +call_id: string, +/** + * Turn ID that this dynamic tool call belongs to. + */ +turn_id: string, +/** + * Dynamic tool name. + */ +tool: string, +/** + * Dynamic tool call arguments. + */ +arguments: JsonValue, +/** + * Dynamic tool response content items. + */ +content_items: Array, +/** + * Whether the tool call succeeded. + */ +success: boolean, +/** + * Optional error text when the tool call failed before producing a response. + */ +error: string | null, +/** + * The duration of the dynamic tool call. + */ +duration: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts index cc1009bfd5..5ccbe1e697 100644 --- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts +++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts @@ -24,6 +24,7 @@ import type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; import type { ContextCompactedEvent } from "./ContextCompactedEvent"; import type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent"; import type { DynamicToolCallRequest } from "./DynamicToolCallRequest"; +import type { DynamicToolCallResponseEvent } from "./DynamicToolCallResponseEvent"; import type { ElicitationRequestEvent } from "./ElicitationRequestEvent"; import type { ErrorEvent } from "./ErrorEvent"; import type { ExecApprovalRequestEvent } from "./ExecApprovalRequestEvent"; @@ -56,7 +57,6 @@ import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent"; import type { RequestUserInputEvent } from "./RequestUserInputEvent"; import type { ReviewRequest } from "./ReviewRequest"; import type { SessionConfiguredEvent } from "./SessionConfiguredEvent"; -import type { SkillRequestApprovalEvent } from "./SkillRequestApprovalEvent"; import type { StreamErrorEvent } from "./StreamErrorEvent"; import type { TerminalInteractionEvent } from "./TerminalInteractionEvent"; import type { ThreadNameUpdatedEvent } from "./ThreadNameUpdatedEvent"; @@ -79,4 +79,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent"; * Response event from the agent * NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen. */ -export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "skill_request_approval" } & SkillRequestApprovalEvent | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; +export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "realtime_conversation_started" } & RealtimeConversationStartedEvent | { "type": "realtime_conversation_realtime" } & RealtimeConversationRealtimeEvent | { "type": "realtime_conversation_closed" } & RealtimeConversationClosedEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "dynamic_tool_call_response" } & DynamicToolCallResponseEvent | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent; diff --git a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts index c777831e89..cc13400900 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts @@ -1,11 +1,12 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AdditionalPermissions } from "./AdditionalPermissions"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; import type { ParsedCommand } from "./ParsedCommand"; +import type { PermissionProfile } from "./PermissionProfile"; +import type { ReviewDecision } from "./ReviewDecision"; export type ExecApprovalRequestEvent = { /** @@ -51,4 +52,11 @@ proposed_network_policy_amendments?: Array, /** * Optional additional filesystem permissions requested for this command. */ -additional_permissions?: AdditionalPermissions, parsed_cmd: Array, }; +additional_permissions?: PermissionProfile, +/** + * Ordered list of decisions the client may present for this prompt. + * + * When absent, clients should derive the legacy default set from the + * other fields on this request. + */ +available_decisions?: Array, parsed_cmd: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/FileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/FileSystemPermissions.ts new file mode 100644 index 0000000000..aedf84de80 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/FileSystemPermissions.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "./AbsolutePathBuf"; + +export type FileSystemPermissions = { read: Array | null, write: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/MacOsAutomationValue.ts similarity index 65% rename from codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.ts rename to codex-rs/app-server-protocol/schema/typescript/MacOsAutomationValue.ts index 3f70fea595..e351c319dd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/MacOsAutomationValue.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SkillRequestApprovalParams = { itemId: string, skillName: string, }; +export type MacOsAutomationValue = boolean | Array; diff --git a/codex-rs/app-server-protocol/schema/typescript/MacOsPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/MacOsPermissions.ts new file mode 100644 index 0000000000..5c0792412c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/MacOsPermissions.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MacOsAutomationValue } from "./MacOsAutomationValue"; +import type { MacOsPreferencesValue } from "./MacOsPreferencesValue"; + +export type MacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.ts b/codex-rs/app-server-protocol/schema/typescript/MacOsPreferencesValue.ts similarity index 72% rename from codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.ts rename to codex-rs/app-server-protocol/schema/typescript/MacOsPreferencesValue.ts index 6773b0f1e0..74a67ca1cb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillApprovalDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/MacOsPreferencesValue.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SkillApprovalDecision = "approve" | "decline"; +export type MacOsPreferencesValue = boolean | string; diff --git a/codex-rs/app-server-protocol/schema/typescript/PermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/PermissionProfile.ts new file mode 100644 index 0000000000..bfc0de27b7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/PermissionProfile.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FileSystemPermissions } from "./FileSystemPermissions"; +import type { MacOsPermissions } from "./MacOsPermissions"; + +export type PermissionProfile = { network: boolean | null, file_system: FileSystemPermissions | null, macos: MacOsPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index 611c7fb22d..dd7621f01d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -15,4 +15,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array | null, write: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts new file mode 100644 index 0000000000..eae1ad810c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalMacOsPermissions.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MacOsAutomationValue } from "../MacOsAutomationValue"; +import type { MacOsPreferencesValue } from "../MacOsPreferencesValue"; + +export type AdditionalMacOsPermissions = { preferences: MacOsPreferencesValue | null, automations: MacOsAutomationValue | null, accessibility: boolean | null, calendar: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts new file mode 100644 index 0000000000..4ca4dca120 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +import type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions"; + +export type AdditionalPermissionProfile = { network: boolean | null, fileSystem: AdditionalFileSystemPermissions | null, macos: AdditionalMacOsPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts new file mode 100644 index 0000000000..83adc6446f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CollaborationModeMask.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ModeKind } from "../ModeKind"; +import type { ReasoningEffort } from "../ReasoningEffort"; + +/** + * EXPERIMENTAL - collaboration mode preset metadata for clients. + */ +export type CollaborationModeMask = { name: string, mode: ModeKind | null, model: string | null, reasoning_effort: ReasoningEffort | null | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts index 80df9bd02c..c022030a1e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionApprovalDecision.ts @@ -2,5 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; +import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | "decline" | "cancel"; +export type CommandExecutionApprovalDecision = "accept" | "acceptForSession" | { "acceptWithExecpolicyAmendment": { execpolicy_amendment: ExecPolicyAmendment, } } | { "applyNetworkPolicyAmendment": { network_policy_amendment: NetworkPolicyAmendment, } } | "decline" | "cancel"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index 4021e50a6a..8fb6375e69 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -1,9 +1,12 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; +import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; +import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string, /** @@ -21,7 +24,7 @@ approvalId?: string | null, */ reason?: string | null, /** - * Optional context for managed-network approval prompts. + * Optional context for a managed-network approval prompt. */ networkApprovalContext?: NetworkApprovalContext | null, /** @@ -36,7 +39,19 @@ cwd?: string | null, * Best-effort parsed command actions for friendly display. */ commandActions?: Array | null, +/** + * Optional additional permissions requested for this command. + */ +additionalPermissions?: AdditionalPermissionProfile | null, /** * Optional proposed execpolicy amendment to allow similar commands without prompting. */ -proposedExecpolicyAmendment?: ExecPolicyAmendment | null, }; +proposedExecpolicyAmendment?: ExecPolicyAmendment | null, +/** + * Optional proposed network policy amendments (allow/deny host) for future requests. + */ +proposedNetworkPolicyAmendments?: Array | null, +/** + * Ordered list of decisions the client may present for this prompt. + */ +availableDecisions?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.ts similarity index 65% rename from codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.ts rename to codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.ts index 6699eadac3..04f44ec0a8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/SkillRequestApprovalEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/DynamicToolCallStatus.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SkillRequestApprovalEvent = { item_id: string, skill_name: string, }; +export type DynamicToolCallStatus = "inProgress" | "completed" | "failed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectParams.ts new file mode 100644 index 0000000000..2a79b8107a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectParams.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigDetectParams = { +/** + * If true, include detection under the user's home (~/.claude, ~/.codex, etc.). + */ +includeHome?: boolean, +/** + * Zero or more working directories to include for repo-scoped detection. + */ +cwds?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectResponse.ts new file mode 100644 index 0000000000..f220e3b616 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigDetectResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; + +export type ExternalAgentConfigDetectResponse = { items: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportParams.ts new file mode 100644 index 0000000000..7bc5d9d91f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; + +export type ExternalAgentConfigImportParams = { migrationItems: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/AdditionalPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportResponse.ts similarity index 62% rename from codex-rs/app-server-protocol/schema/typescript/AdditionalPermissions.ts rename to codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportResponse.ts index 363807a2aa..2ceddade0e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/AdditionalPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigImportResponse.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AdditionalPermissions = { fs_read?: Array, fs_write?: Array, }; +export type ExternalAgentConfigImportResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItem.ts new file mode 100644 index 0000000000..442143c81e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItem.ts @@ -0,0 +1,10 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; + +export type ExternalAgentConfigMigrationItem = { itemType: ExternalAgentConfigMigrationItemType, description: string, +/** + * Null or empty means home-scoped migration; non-empty means repo-scoped migration. + */ +cwd: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItemType.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItemType.ts new file mode 100644 index 0000000000..c9bd160b1c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ExternalAgentConfigMigrationItemType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExternalAgentConfigMigrationItemType = "AGENTS_MD" | "CONFIG" | "SKILLS" | "MCP_SERVER_CONFIG"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts index de7b240173..cd99108190 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts @@ -3,6 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { InputModality } from "../InputModality"; import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; +import type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; import type { ReasoningEffortOption } from "./ReasoningEffortOption"; -export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; +export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts new file mode 100644 index 0000000000..7254aaec9b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelAvailabilityNux = { message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts new file mode 100644 index 0000000000..82d73e9d06 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ModelUpgradeInfo = { model: string, upgradeCopy: string | null, modelLink: string | null, migrationMarkdown: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyAmendment.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyAmendment.ts new file mode 100644 index 0000000000..4e5092e4d0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyAmendment.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; + +export type NetworkPolicyAmendment = { host: string, action: NetworkPolicyRuleAction, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyRuleAction.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyRuleAction.ts new file mode 100644 index 0000000000..55ec70032a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkPolicyRuleAction.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkPolicyRuleAction = "allow" | "deny"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts deleted file mode 100644 index 9ce454ff4f..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillRequestApprovalResponse.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SkillApprovalDecision } from "./SkillApprovalDecision"; - -export type SkillRequestApprovalResponse = { decision: SkillApprovalDecision, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadClosedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadClosedNotification.ts new file mode 100644 index 0000000000..ed5bf546ca --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadClosedNotification.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadClosedNotification = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index dee39cd5db..d937d53b44 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -8,6 +8,8 @@ import type { CollabAgentTool } from "./CollabAgentTool"; import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; import type { CommandAction } from "./CommandAction"; import type { CommandExecutionStatus } from "./CommandExecutionStatus"; +import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; +import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; import type { FileUpdateChange } from "./FileUpdateChange"; import type { McpToolCallError } from "./McpToolCallError"; import type { McpToolCallResult } from "./McpToolCallResult"; @@ -50,6 +52,10 @@ durationMs: number | null, } | { "type": "fileChange", id: string, changes: Arra /** * The duration of the MCP tool call in milliseconds. */ +durationMs: number | null, } | { "type": "dynamicToolCall", id: string, tool: string, arguments: JsonValue, status: DynamicToolCallStatus, contentItems: Array | null, success: boolean | null, +/** + * The duration of the dynamic tool call in milliseconds. + */ durationMs: number | null, } | { "type": "collabAgentToolCall", /** * Unique identifier for this collab tool call. diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadListParams.ts index 7fdadbad67..ea14ec3d45 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadListParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadListParams.ts @@ -36,4 +36,8 @@ archived?: boolean | null, * Optional cwd filter; when set, only threads whose session cwd exactly * matches this path are returned. */ -cwd?: string | null, }; +cwd?: string | null, +/** + * Optional substring filter for the extracted thread title. + */ +searchTerm?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeAudioChunk.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeAudioChunk.ts new file mode 100644 index 0000000000..078f642247 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeAudioChunk.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - thread realtime audio chunk. + */ +export type ThreadRealtimeAudioChunk = { data: string, sampleRate: number, numChannels: number, samplesPerChannel: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeClosedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeClosedNotification.ts new file mode 100644 index 0000000000..a39cd71ed7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeClosedNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted when thread realtime transport closes. + */ +export type ThreadRealtimeClosedNotification = { threadId: string, reason: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeErrorNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeErrorNotification.ts new file mode 100644 index 0000000000..0b24879ecd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeErrorNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted when thread realtime encounters an error. + */ +export type ThreadRealtimeErrorNotification = { threadId: string, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeItemAddedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeItemAddedNotification.ts new file mode 100644 index 0000000000..f996e77cd8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeItemAddedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +/** + * EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. + */ +export type ThreadRealtimeItemAddedNotification = { threadId: string, item: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts new file mode 100644 index 0000000000..1d03fd89cf --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeOutputAudioDeltaNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; + +/** + * EXPERIMENTAL - streamed output audio emitted by thread realtime. + */ +export type ThreadRealtimeOutputAudioDeltaNotification = { threadId: string, audio: ThreadRealtimeAudioChunk, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeStartedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeStartedNotification.ts new file mode 100644 index 0000000000..736ecde1fe --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeStartedNotification.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - emitted when thread realtime startup is accepted. + */ +export type ThreadRealtimeStartedNotification = { threadId: string, sessionId: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index ed5e89e73c..5f34f6198c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,7 +6,7 @@ import type { JsonValue } from "../serde_json/JsonValue"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxMode } from "./SandboxMode"; -export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /** +export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /** * If true, opt into emitting raw Responses API items on the event stream. * This is for internal use only (e.g. Codex Cloud). */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeParams.ts new file mode 100644 index 0000000000..3d5f3a04cd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeParams.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnsubscribeParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeResponse.ts new file mode 100644 index 0000000000..6f8f66b226 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; + +export type ThreadUnsubscribeResponse = { status: ThreadUnsubscribeStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeStatus.ts new file mode 100644 index 0000000000..2970598dc1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUnsubscribeStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ThreadUnsubscribeStatus = "notLoaded" | "notSubscribed" | "unsubscribed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 12e6e9adb1..4a881427d9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -4,6 +4,9 @@ export type { Account } from "./Account"; export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; +export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; +export type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions"; +export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; export type { AnalyticsConfig } from "./AnalyticsConfig"; export type { AppBranding } from "./AppBranding"; @@ -31,6 +34,7 @@ export type { CollabAgentState } from "./CollabAgentState"; export type { CollabAgentStatus } from "./CollabAgentStatus"; export type { CollabAgentTool } from "./CollabAgentTool"; export type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; +export type { CollaborationModeMask } from "./CollaborationModeMask"; export type { CommandAction } from "./CommandAction"; export type { CommandExecParams } from "./CommandExecParams"; export type { CommandExecResponse } from "./CommandExecResponse"; @@ -58,6 +62,7 @@ export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotificat export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; +export type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; export type { DynamicToolSpec } from "./DynamicToolSpec"; export type { ErrorNotification } from "./ErrorNotification"; export type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; @@ -65,6 +70,12 @@ export type { ExperimentalFeature } from "./ExperimentalFeature"; export type { ExperimentalFeatureListParams } from "./ExperimentalFeatureListParams"; export type { ExperimentalFeatureListResponse } from "./ExperimentalFeatureListResponse"; export type { ExperimentalFeatureStage } from "./ExperimentalFeatureStage"; +export type { ExternalAgentConfigDetectParams } from "./ExternalAgentConfigDetectParams"; +export type { ExternalAgentConfigDetectResponse } from "./ExternalAgentConfigDetectResponse"; +export type { ExternalAgentConfigImportParams } from "./ExternalAgentConfigImportParams"; +export type { ExternalAgentConfigImportResponse } from "./ExternalAgentConfigImportResponse"; +export type { ExternalAgentConfigMigrationItem } from "./ExternalAgentConfigMigrationItem"; +export type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType"; export type { FeedbackUploadParams } from "./FeedbackUploadParams"; export type { FeedbackUploadResponse } from "./FeedbackUploadResponse"; export type { FileChangeApprovalDecision } from "./FileChangeApprovalDecision"; @@ -96,13 +107,17 @@ export type { McpToolCallResult } from "./McpToolCallResult"; export type { McpToolCallStatus } from "./McpToolCallStatus"; export type { MergeStrategy } from "./MergeStrategy"; export type { Model } from "./Model"; +export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; export type { ModelRerouteReason } from "./ModelRerouteReason"; export type { ModelReroutedNotification } from "./ModelReroutedNotification"; +export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkApprovalContext } from "./NetworkApprovalContext"; export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; +export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; +export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; export type { NetworkRequirements } from "./NetworkRequirements"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; @@ -128,13 +143,10 @@ export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; export type { SessionSource } from "./SessionSource"; -export type { SkillApprovalDecision } from "./SkillApprovalDecision"; export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; export type { SkillInterface } from "./SkillInterface"; export type { SkillMetadata } from "./SkillMetadata"; -export type { SkillRequestApprovalParams } from "./SkillRequestApprovalParams"; -export type { SkillRequestApprovalResponse } from "./SkillRequestApprovalResponse"; export type { SkillScope } from "./SkillScope"; export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; @@ -156,6 +168,7 @@ export type { ThreadActiveFlag } from "./ThreadActiveFlag"; export type { ThreadArchiveParams } from "./ThreadArchiveParams"; export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; +export type { ThreadClosedNotification } from "./ThreadClosedNotification"; export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; export type { ThreadForkParams } from "./ThreadForkParams"; @@ -168,6 +181,12 @@ export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse"; export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification"; export type { ThreadReadParams } from "./ThreadReadParams"; export type { ThreadReadResponse } from "./ThreadReadResponse"; +export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk"; +export type { ThreadRealtimeClosedNotification } from "./ThreadRealtimeClosedNotification"; +export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotification"; +export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification"; +export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification"; +export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification"; export type { ThreadResumeParams } from "./ThreadResumeParams"; export type { ThreadResumeResponse } from "./ThreadResumeResponse"; export type { ThreadRollbackParams } from "./ThreadRollbackParams"; @@ -186,6 +205,9 @@ export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpda export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; +export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams"; +export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse"; +export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus"; export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index b6c8da90ea..b48651c03c 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -1947,6 +1947,15 @@ mod tests { let thread_start_ts = fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.ts"))?; assert_eq!(thread_start_ts.contains("mockExperimentalField"), true); + let command_execution_request_approval_ts = fs::read_to_string( + output_dir + .join("v2") + .join("CommandExecutionRequestApprovalParams.ts"), + )?; + assert_eq!( + command_execution_request_approval_ts.contains("additionalPermissions"), + true + ); Ok(()) } @@ -2083,6 +2092,12 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k let thread_start_json = fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.json"))?; assert_eq!(thread_start_json.contains("mockExperimentalField"), false); + let command_execution_request_approval_json = + fs::read_to_string(output_dir.join("CommandExecutionRequestApprovalParams.json"))?; + assert_eq!( + command_execution_request_approval_json.contains("additionalPermissions"), + false + ); let client_request_json = fs::read_to_string(output_dir.join("ClientRequest.json"))?; assert_eq!( @@ -2093,6 +2108,7 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k let bundle_json = fs::read_to_string(output_dir.join("codex_app_server_protocol.schemas.json"))?; assert_eq!(bundle_json.contains("mockExperimentalField"), false); + assert_eq!(bundle_json.contains("additionalPermissions"), false); assert_eq!(bundle_json.contains("MockExperimentalMethodParams"), false); assert_eq!( bundle_json.contains("MockExperimentalMethodResponse"), diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index f32925746f..9a96aefc2e 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -7,6 +7,7 @@ use crate::export::GeneratedSchema; use crate::export::write_json_schema; use crate::protocol::v1; use crate::protocol::v2; +use codex_experimental_api_macros::ExperimentalApi; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -202,6 +203,10 @@ client_request_definitions! { params: v2::ThreadArchiveParams, response: v2::ThreadArchiveResponse, }, + ThreadUnsubscribe => "thread/unsubscribe" { + params: v2::ThreadUnsubscribeParams, + response: v2::ThreadUnsubscribeResponse, + }, ThreadSetName => "thread/name/set" { params: v2::ThreadSetNameParams, response: v2::ThreadSetNameResponse, @@ -268,6 +273,26 @@ client_request_definitions! { params: v2::TurnInterruptParams, response: v2::TurnInterruptResponse, }, + #[experimental("thread/realtime/start")] + ThreadRealtimeStart => "thread/realtime/start" { + params: v2::ThreadRealtimeStartParams, + response: v2::ThreadRealtimeStartResponse, + }, + #[experimental("thread/realtime/appendAudio")] + ThreadRealtimeAppendAudio => "thread/realtime/appendAudio" { + params: v2::ThreadRealtimeAppendAudioParams, + response: v2::ThreadRealtimeAppendAudioResponse, + }, + #[experimental("thread/realtime/appendText")] + ThreadRealtimeAppendText => "thread/realtime/appendText" { + params: v2::ThreadRealtimeAppendTextParams, + response: v2::ThreadRealtimeAppendTextResponse, + }, + #[experimental("thread/realtime/stop")] + ThreadRealtimeStop => "thread/realtime/stop" { + params: v2::ThreadRealtimeStopParams, + response: v2::ThreadRealtimeStopResponse, + }, ReviewStart => "review/start" { params: v2::ReviewStartParams, response: v2::ReviewStartResponse, @@ -350,6 +375,14 @@ client_request_definitions! { params: v2::ConfigReadParams, response: v2::ConfigReadResponse, }, + ExternalAgentConfigDetect => "externalAgentConfig/detect" { + params: v2::ExternalAgentConfigDetectParams, + response: v2::ExternalAgentConfigDetectResponse, + }, + ExternalAgentConfigImport => "externalAgentConfig/import" { + params: v2::ExternalAgentConfigImportParams, + response: v2::ExternalAgentConfigImportResponse, + }, ConfigValueWrite => "config/value/write" { params: v2::ConfigValueWriteParams, response: v2::ConfigWriteResponse, @@ -501,6 +534,7 @@ macro_rules! server_request_definitions { ) => { /// Request initiated from the server and sent to the client. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] + #[allow(clippy::large_enum_variant)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ServerRequest { $( @@ -515,6 +549,7 @@ macro_rules! server_request_definitions { } #[derive(Debug, Clone, PartialEq, JsonSchema)] + #[allow(clippy::large_enum_variant)] pub enum ServerRequestPayload { $( $variant($params), )* } @@ -576,7 +611,16 @@ macro_rules! server_notification_definitions { ),* $(,)? ) => { /// Notification sent from the server to the client. - #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)] + #[derive( + Serialize, + Deserialize, + Debug, + Clone, + JsonSchema, + TS, + Display, + ExperimentalApi, + )] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ServerNotification { @@ -671,11 +715,6 @@ server_request_definitions! { response: v2::ToolRequestUserInputResponse, }, - SkillRequestApproval => "skill/requestApproval" { - params: v2::SkillRequestApprovalParams, - response: v2::SkillRequestApprovalResponse, - }, - /// Execute a dynamic tool call on the client. DynamicToolCall => "item/tool/call" { params: v2::DynamicToolCallParams, @@ -782,6 +821,7 @@ server_notification_definitions! { ThreadStatusChanged => "thread/status/changed" (v2::ThreadStatusChangedNotification), ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification), ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification), + ThreadClosed => "thread/closed" (v2::ThreadClosedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification), TurnStarted => "turn/started" (v2::TurnStartedNotification), @@ -813,6 +853,16 @@ server_notification_definitions! { ConfigWarning => "configWarning" (v2::ConfigWarningNotification), FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification), FuzzyFileSearchSessionCompleted => "fuzzyFileSearch/sessionCompleted" (FuzzyFileSearchSessionCompletedNotification), + #[experimental("thread/realtime/started")] + ThreadRealtimeStarted => "thread/realtime/started" (v2::ThreadRealtimeStartedNotification), + #[experimental("thread/realtime/itemAdded")] + ThreadRealtimeItemAdded => "thread/realtime/itemAdded" (v2::ThreadRealtimeItemAddedNotification), + #[experimental("thread/realtime/outputAudio/delta")] + ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification), + #[experimental("thread/realtime/error")] + ThreadRealtimeError => "thread/realtime/error" (v2::ThreadRealtimeErrorNotification), + #[experimental("thread/realtime/closed")] + ThreadRealtimeClosed => "thread/realtime/closed" (v2::ThreadRealtimeClosedNotification), /// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox. WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification), @@ -843,10 +893,15 @@ mod tests { use codex_protocol::account::PlanType; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AskForApproval; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(path).expect("absolute path") + } + #[test] fn serialize_new_conversation() -> Result<()> { let request = ClientRequest::NewConversation { @@ -1340,6 +1395,31 @@ mod tests { Ok(()) } + #[test] + fn serialize_thread_realtime_start() -> Result<()> { + let request = ClientRequest::ThreadRealtimeStart { + request_id: RequestId::Integer(9), + params: v2::ThreadRealtimeStartParams { + thread_id: "thr_123".to_string(), + prompt: "You are on a call".to_string(), + session_id: Some("sess_456".to_string()), + }, + }; + assert_eq!( + json!({ + "method": "thread/realtime/start", + "id": 9, + "params": { + "threadId": "thr_123", + "prompt": "You are on a call", + "sessionId": "sess_456" + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + #[test] fn serialize_thread_status_changed_notification() -> Result<()> { let notification = @@ -1362,6 +1442,37 @@ mod tests { Ok(()) } + #[test] + fn serialize_thread_realtime_output_audio_delta_notification() -> Result<()> { + let notification = ServerNotification::ThreadRealtimeOutputAudioDelta( + v2::ThreadRealtimeOutputAudioDeltaNotification { + thread_id: "thr_123".to_string(), + audio: v2::ThreadRealtimeAudioChunk { + data: "AQID".to_string(), + sample_rate: 24_000, + num_channels: 1, + samples_per_channel: Some(512), + }, + }, + ); + assert_eq!( + json!({ + "method": "thread/realtime/outputAudio/delta", + "params": { + "threadId": "thr_123", + "audio": { + "data": "AQID", + "sampleRate": 24000, + "numChannels": 1, + "samplesPerChannel": 512 + } + } + }), + serde_json::to_value(¬ification)?, + ); + Ok(()) + } + #[test] fn mock_experimental_method_is_marked_experimental() { let request = ClientRequest::MockExperimentalMethod { @@ -1371,4 +1482,75 @@ mod tests { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); assert_eq!(reason, Some("mock/experimentalMethod")); } + #[test] + fn thread_realtime_start_is_marked_experimental() { + let request = ClientRequest::ThreadRealtimeStart { + request_id: RequestId::Integer(1), + params: v2::ThreadRealtimeStartParams { + thread_id: "thr_123".to_string(), + prompt: "You are on a call".to_string(), + session_id: None, + }, + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("thread/realtime/start")); + } + #[test] + fn thread_realtime_started_notification_is_marked_experimental() { + let notification = + ServerNotification::ThreadRealtimeStarted(v2::ThreadRealtimeStartedNotification { + thread_id: "thr_123".to_string(), + session_id: Some("sess_456".to_string()), + }); + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¬ification); + assert_eq!(reason, Some("thread/realtime/started")); + } + + #[test] + fn thread_realtime_output_audio_delta_notification_is_marked_experimental() { + let notification = ServerNotification::ThreadRealtimeOutputAudioDelta( + v2::ThreadRealtimeOutputAudioDeltaNotification { + thread_id: "thr_123".to_string(), + audio: v2::ThreadRealtimeAudioChunk { + data: "AQID".to_string(), + sample_rate: 24_000, + num_channels: 1, + samples_per_channel: Some(512), + }, + }, + ); + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¬ification); + assert_eq!(reason, Some("thread/realtime/outputAudio/delta")); + } + + #[test] + fn command_execution_request_approval_additional_permissions_is_marked_experimental() { + let params = v2::CommandExecutionRequestApprovalParams { + thread_id: "thr_123".to_string(), + turn_id: "turn_123".to_string(), + item_id: "call_123".to_string(), + approval_id: None, + reason: None, + network_approval_context: None, + command: Some("cat file".to_string()), + cwd: None, + command_actions: None, + additional_permissions: Some(v2::AdditionalPermissionProfile { + network: None, + file_system: Some(v2::AdditionalFileSystemPermissions { + read: Some(vec![absolute_path("/tmp/allowed")]), + write: None, + }), + macos: None, + }), + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + available_decisions: None, + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¶ms); + assert_eq!( + reason, + Some("item/commandExecution/requestApproval.additionalPermissions") + ); + } } diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index c6d0cc00fe..97ad8d521f 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -3,6 +3,8 @@ use crate::protocol::v2::CollabAgentTool; use crate::protocol::v2::CollabAgentToolCallStatus; use crate::protocol::v2::CommandAction; use crate::protocol::v2::CommandExecutionStatus; +use crate::protocol::v2::DynamicToolCallOutputContentItem; +use crate::protocol::v2::DynamicToolCallStatus; use crate::protocol::v2::FileUpdateChange; use crate::protocol::v2::McpToolCallError; use crate::protocol::v2::McpToolCallResult; @@ -22,6 +24,7 @@ use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::CompactedItem; use codex_protocol::protocol::ContextCompactedEvent; +use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; @@ -125,6 +128,12 @@ impl ThreadHistoryBuilder { EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload), EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload), EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload), + EventMsg::DynamicToolCallRequest(payload) => { + self.handle_dynamic_tool_call_request(payload) + } + EventMsg::DynamicToolCallResponse(payload) => { + self.handle_dynamic_tool_call_response(payload) + } EventMsg::McpToolCallBegin(payload) => self.handle_mcp_tool_call_begin(payload), EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload), EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload), @@ -382,6 +391,49 @@ impl ThreadHistoryBuilder { } } + fn handle_dynamic_tool_call_request( + &mut self, + payload: &codex_protocol::dynamic_tools::DynamicToolCallRequest, + ) { + let item = ThreadItem::DynamicToolCall { + id: payload.call_id.clone(), + tool: payload.tool.clone(), + arguments: payload.arguments.clone(), + status: DynamicToolCallStatus::InProgress, + content_items: None, + success: None, + duration_ms: None, + }; + if payload.turn_id.is_empty() { + self.upsert_item_in_current_turn(item); + } else { + self.upsert_item_in_turn_id(&payload.turn_id, item); + } + } + + fn handle_dynamic_tool_call_response(&mut self, payload: &DynamicToolCallResponseEvent) { + let status = if payload.success { + DynamicToolCallStatus::Completed + } else { + DynamicToolCallStatus::Failed + }; + let duration_ms = i64::try_from(payload.duration.as_millis()).ok(); + let item = ThreadItem::DynamicToolCall { + id: payload.call_id.clone(), + tool: payload.tool.clone(), + arguments: payload.arguments.clone(), + status, + content_items: Some(convert_dynamic_tool_content_items(&payload.content_items)), + success: Some(payload.success), + duration_ms, + }; + if payload.turn_id.is_empty() { + self.upsert_item_in_current_turn(item); + } else { + self.upsert_item_in_turn_id(&payload.turn_id, item); + } + } + fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) { let item = ThreadItem::McpToolCall { id: payload.call_id.clone(), @@ -913,6 +965,23 @@ pub fn convert_patch_changes( converted } +fn convert_dynamic_tool_content_items( + items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem], +) -> Vec { + items + .iter() + .cloned() + .map(|item| match item { + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => { + DynamicToolCallOutputContentItem::InputText { text } + } + codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { + image_url, + } => DynamicToolCallOutputContentItem::InputImage { image_url }, + }) + .collect() +} + fn map_patch_change_kind(change: &codex_protocol::protocol::FileChange) -> PatchChangeKind { match change { codex_protocol::protocol::FileChange::Add { .. } => PatchChangeKind::Add, @@ -1002,6 +1071,7 @@ impl From<&PendingTurn> for Turn { mod tests { use super::*; use codex_protocol::ThreadId; + use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::models::MessagePhase as CoreMessagePhase; @@ -1012,6 +1082,7 @@ mod tests { use codex_protocol::protocol::AgentReasoningRawContentEvent; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CompactedItem; + use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ItemStartedEvent; @@ -1606,6 +1677,65 @@ mod tests { ); } + #[test] + fn reconstructs_dynamic_tool_items_from_request_and_response_events() { + let events = vec![ + EventMsg::TurnStarted(TurnStartedEvent { + turn_id: "turn-1".into(), + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + EventMsg::UserMessage(UserMessageEvent { + message: "run dynamic tool".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + }), + EventMsg::DynamicToolCallRequest( + codex_protocol::dynamic_tools::DynamicToolCallRequest { + call_id: "dyn-1".into(), + turn_id: "turn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + }, + ), + EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id: "dyn-1".into(), + turn_id: "turn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { + text: "Ticket is open".into(), + }], + success: true, + error: None, + duration: Duration::from_millis(42), + }), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].items.len(), 2); + assert_eq!( + turns[0].items[1], + ThreadItem::DynamicToolCall { + id: "dyn-1".into(), + tool: "lookup_ticket".into(), + arguments: serde_json::json!({"id":"ABC-123"}), + status: DynamicToolCallStatus::Completed, + content_items: Some(vec![DynamicToolCallOutputContentItem::InputText { + text: "Ticket is open".into(), + }]), + success: Some(true), + duration_ms: Some(42), + } + ); + } + #[test] fn reconstructs_declined_exec_and_patch_items() { let events = vec![ diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 976f4e69d8..719268e9c5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -531,6 +531,15 @@ impl From for CoreTextElement { } } +impl InputItem { + pub fn text_char_count(&self) -> usize { + match self { + InputItem::Text { text, .. } => text.chars().count(), + InputItem::Image { .. } | InputItem::LocalImage { .. } => 0, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] /// Deprecated in favor of AccountLoginCompletedNotification. diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index b1f6ceb5c1..8052eb080e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -7,9 +7,12 @@ use codex_protocol::account::PlanType; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; +use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment; +use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction; use codex_protocol::config_types::CollaborationMode; -use codex_protocol::config_types::CollaborationModeMask; +use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; use codex_protocol::config_types::ForcedLoginMethod; +use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode as CoreSandboxMode; @@ -20,9 +23,15 @@ use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::mcp::Resource as McpResource; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; +use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; +use codex_protocol::models::MacOsAutomationValue as CoreMacOsAutomationValue; +use codex_protocol::models::MacOsPermissions as CoreMacOsPermissions; +use codex_protocol::models::MacOsPreferencesValue as CoreMacOsPreferencesValue; use codex_protocol::models::MessagePhase; +use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; @@ -39,7 +48,9 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; +use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; use codex_protocol::protocol::RejectConfig as CoreRejectConfig; +use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; @@ -633,6 +644,64 @@ pub struct ConfigRequirementsReadResponse { pub requirements: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub enum ExternalAgentConfigMigrationItemType { + #[serde(rename = "AGENTS_MD")] + #[ts(rename = "AGENTS_MD")] + AgentsMd, + #[serde(rename = "CONFIG")] + #[ts(rename = "CONFIG")] + Config, + #[serde(rename = "SKILLS")] + #[ts(rename = "SKILLS")] + Skills, + #[serde(rename = "MCP_SERVER_CONFIG")] + #[ts(rename = "MCP_SERVER_CONFIG")] + McpServerConfig, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigMigrationItem { + pub item_type: ExternalAgentConfigMigrationItemType, + pub description: String, + /// Null or empty means home-scoped migration; non-empty means repo-scoped migration. + pub cwd: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigDetectResponse { + pub items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigDetectParams { + /// If true, include detection under the user's home (~/.claude, ~/.codex, etc.). + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub include_home: bool, + /// Zero or more working directories to include for repo-scoped detection. + #[ts(optional = nullable)] + pub cwds: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigImportParams { + pub migration_items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigImportResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -674,19 +743,45 @@ pub struct ConfigEdit { pub enum CommandExecutionApprovalDecision { /// User approved the command. Accept, - /// User approved the command and future identical commands should run without prompting. + /// User approved the command and future prompts in the same session-scoped + /// approval cache should run without prompting. AcceptForSession, /// User approved the command, and wants to apply the proposed execpolicy amendment so future /// matching commands can run without prompting. AcceptWithExecpolicyAmendment { execpolicy_amendment: ExecPolicyAmendment, }, + /// User chose a persistent network policy rule (allow/deny) for this host. + ApplyNetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment, + }, /// User denied the command. The agent will continue the turn. Decline, /// User denied the command. The turn will also be immediately interrupted. Cancel, } +impl From for CommandExecutionApprovalDecision { + fn from(value: CoreReviewDecision) -> Self { + match value { + CoreReviewDecision::Approved => Self::Accept, + CoreReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => Self::AcceptWithExecpolicyAmendment { + execpolicy_amendment: proposed_execpolicy_amendment.into(), + }, + CoreReviewDecision::ApprovedForSession => Self::AcceptForSession, + CoreReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => Self::ApplyNetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.into(), + }, + CoreReviewDecision::Abort => Self::Cancel, + CoreReviewDecision::Denied => Self::Decline, + } + } +} + v2_enum_from_core! { pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol { Http, @@ -713,6 +808,63 @@ impl From for NetworkApprovalContext { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalFileSystemPermissions { + pub read: Option>, + pub write: Option>, +} + +impl From for AdditionalFileSystemPermissions { + fn from(value: CoreFileSystemPermissions) -> Self { + Self { + read: value.read, + write: value.write, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalMacOsPermissions { + pub preferences: Option, + pub automations: Option, + pub accessibility: Option, + pub calendar: Option, +} + +impl From for AdditionalMacOsPermissions { + fn from(value: CoreMacOsPermissions) -> Self { + Self { + preferences: value.preferences, + automations: value.automations, + accessibility: value.accessibility, + calendar: value.calendar, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalPermissionProfile { + pub network: Option, + pub file_system: Option, + pub macos: Option, +} + +impl From for AdditionalPermissionProfile { + fn from(value: CorePermissionProfile) -> Self { + Self { + network: value.network, + file_system: value.file_system.map(AdditionalFileSystemPermissions::from), + macos: value.macos.map(AdditionalMacOsPermissions::from), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -910,6 +1062,38 @@ impl From for ExecPolicyAmendment { } } +v2_enum_from_core!( + pub enum NetworkPolicyRuleAction from CoreNetworkPolicyRuleAction { + Allow, Deny + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkPolicyAmendment { + pub host: String, + pub action: NetworkPolicyRuleAction, +} + +impl NetworkPolicyAmendment { + pub fn into_core(self) -> CoreNetworkPolicyAmendment { + CoreNetworkPolicyAmendment { + host: self.host, + action: self.action.to_core(), + } + } +} + +impl From for NetworkPolicyAmendment { + fn from(value: CoreNetworkPolicyAmendment) -> Self { + Self { + host: value.host, + action: NetworkPolicyRuleAction::from(value.action), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -1206,6 +1390,21 @@ pub struct ModelListParams { pub include_hidden: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelAvailabilityNux { + pub message: String, +} + +impl From for ModelAvailabilityNux { + fn from(value: CoreModelAvailabilityNux) -> Self { + Self { + message: value.message, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1213,6 +1412,8 @@ pub struct Model { pub id: String, pub model: String, pub upgrade: Option, + pub upgrade_info: Option, + pub availability_nux: Option, pub display_name: String, pub description: String, pub hidden: bool, @@ -1226,6 +1427,16 @@ pub struct Model { pub is_default: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelUpgradeInfo { + pub model: String, + pub upgrade_copy: Option, + pub model_link: Option, + pub migration_markdown: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1250,6 +1461,30 @@ pub struct ModelListResponse { #[ts(export_to = "v2/")] pub struct CollaborationModeListParams {} +/// EXPERIMENTAL - collaboration mode preset metadata for clients. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollaborationModeMask { + pub name: String, + pub mode: Option, + pub model: Option, + #[serde(rename = "reasoning_effort")] + #[ts(rename = "reasoning_effort")] + pub reasoning_effort: Option>, +} + +impl From for CollaborationModeMask { + fn from(value: CoreCollaborationModeMask) -> Self { + Self { + name: value.name, + mode: value.mode, + model: value.model, + reasoning_effort: value.reasoning_effort, + } + } +} + /// EXPERIMENTAL - collaboration mode presets response. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -1561,6 +1796,8 @@ pub struct ThreadStartParams { #[ts(optional = nullable)] pub config: Option>, #[ts(optional = nullable)] + pub service_name: Option, + #[ts(optional = nullable)] pub base_instructions: Option, #[ts(optional = nullable)] pub developer_instructions: Option, @@ -1757,6 +1994,29 @@ pub struct ThreadArchiveParams { #[ts(export_to = "v2/")] pub struct ThreadArchiveResponse {} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnsubscribeParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnsubscribeResponse { + pub status: ThreadUnsubscribeStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadUnsubscribeStatus { + NotLoaded, + NotSubscribed, + Unsubscribed, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1861,6 +2121,9 @@ pub struct ThreadListParams { /// matches this path are returned. #[ts(optional = nullable)] pub cwd: Option, + /// Optional substring filter for the extracted thread title. + #[ts(optional = nullable)] + pub search_term: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] @@ -2388,6 +2651,157 @@ pub struct ErrorNotification { pub turn_id: String, } +/// EXPERIMENTAL - thread realtime audio chunk. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAudioChunk { + pub data: String, + pub sample_rate: u32, + pub num_channels: u16, + pub samples_per_channel: Option, +} + +impl From for ThreadRealtimeAudioChunk { + fn from(value: CoreRealtimeAudioFrame) -> Self { + let CoreRealtimeAudioFrame { + data, + sample_rate, + num_channels, + samples_per_channel, + } = value; + Self { + data, + sample_rate, + num_channels, + samples_per_channel, + } + } +} + +impl From for CoreRealtimeAudioFrame { + fn from(value: ThreadRealtimeAudioChunk) -> Self { + let ThreadRealtimeAudioChunk { + data, + sample_rate, + num_channels, + samples_per_channel, + } = value; + Self { + data, + sample_rate, + num_channels, + samples_per_channel, + } + } +} + +/// EXPERIMENTAL - start a thread-scoped realtime session. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartParams { + pub thread_id: String, + pub prompt: String, + #[ts(optional = nullable)] + pub session_id: Option, +} + +/// EXPERIMENTAL - response for starting thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartResponse {} + +/// EXPERIMENTAL - append audio input to thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendAudioParams { + pub thread_id: String, + pub audio: ThreadRealtimeAudioChunk, +} + +/// EXPERIMENTAL - response for appending realtime audio input. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendAudioResponse {} + +/// EXPERIMENTAL - append text input to thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendTextParams { + pub thread_id: String, + pub text: String, +} + +/// EXPERIMENTAL - response for appending realtime text input. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendTextResponse {} + +/// EXPERIMENTAL - stop thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStopParams { + pub thread_id: String, +} + +/// EXPERIMENTAL - response for stopping thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStopResponse {} + +/// EXPERIMENTAL - emitted when thread realtime startup is accepted. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartedNotification { + pub thread_id: String, + pub session_id: Option, +} + +/// EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeItemAddedNotification { + pub thread_id: String, + pub item: JsonValue, +} + +/// EXPERIMENTAL - streamed output audio emitted by thread realtime. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeOutputAudioDeltaNotification { + pub thread_id: String, + pub audio: ThreadRealtimeAudioChunk, +} + +/// EXPERIMENTAL - emitted when thread realtime encounters an error. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeErrorNotification { + pub thread_id: String, + pub message: String, +} + +/// EXPERIMENTAL - emitted when thread realtime transport closes. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeClosedNotification { + pub thread_id: String, + pub reason: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -2666,6 +3080,18 @@ impl From for UserInput { } } +impl UserInput { + pub fn text_char_count(&self) -> usize { + match self { + UserInput::Text { text, .. } => text.chars().count(), + UserInput::Image { .. } + | UserInput::LocalImage { .. } + | UserInput::Skill { .. } + | UserInput::Mention { .. } => 0, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -2742,6 +3168,19 @@ pub enum ThreadItem { }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] + DynamicToolCall { + id: String, + tool: String, + arguments: JsonValue, + status: DynamicToolCallStatus, + content_items: Option>, + success: Option, + /// The duration of the dynamic tool call in milliseconds. + #[ts(type = "number | null")] + duration_ms: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] CollabAgentToolCall { /// Unique identifier for this collab tool call. id: String, @@ -2790,6 +3229,7 @@ impl ThreadItem { | ThreadItem::CommandExecution { id, .. } | ThreadItem::FileChange { id, .. } | ThreadItem::McpToolCall { id, .. } + | ThreadItem::DynamicToolCall { id, .. } | ThreadItem::CollabAgentToolCall { id, .. } | ThreadItem::WebSearch { id, .. } | ThreadItem::ImageView { id, .. } @@ -2970,6 +3410,15 @@ pub enum McpToolCallStatus { Failed, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum DynamicToolCallStatus { + InProgress, + Completed, + Failed, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3081,6 +3530,13 @@ pub struct ThreadUnarchivedNotification { pub thread_id: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadClosedNotification { + pub thread_id: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3359,7 +3815,7 @@ pub struct ContextCompactedNotification { pub turn_id: String, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct CommandExecutionRequestApprovalParams { @@ -3380,7 +3836,7 @@ pub struct CommandExecutionRequestApprovalParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub reason: Option, - /// Optional context for managed-network approval prompts. + /// Optional context for a managed-network approval prompt. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub network_approval_context: Option, @@ -3396,10 +3852,33 @@ pub struct CommandExecutionRequestApprovalParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub command_actions: Option>, + /// Optional additional permissions requested for this command. + #[experimental("item/commandExecution/requestApproval.additionalPermissions")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub additional_permissions: Option, /// Optional proposed execpolicy amendment to allow similar commands without prompting. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub proposed_execpolicy_amendment: Option, + /// Optional proposed network policy amendments (allow/deny host) for future requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub proposed_network_policy_amendments: Option>, + /// Ordered list of decisions the client may present for this prompt. + #[experimental("item/commandExecution/requestApproval.availableDecisions")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub available_decisions: Option>, +} + +impl CommandExecutionRequestApprovalParams { + pub fn strip_experimental_fields(&mut self) { + // TODO: Avoid hardcoding individual experimental fields here. + // We need a generic outbound compatibility design for stripping or + // otherwise handling experimental server->client payloads. + self.additional_permissions = None; + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -3431,29 +3910,6 @@ pub struct FileChangeRequestApprovalResponse { pub decision: FileChangeApprovalDecision, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillRequestApprovalParams { - pub item_id: String, - pub skill_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum SkillApprovalDecision { - Approve, - Decline, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillRequestApprovalResponse { - pub decision: SkillApprovalDecision, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3714,6 +4170,37 @@ mod tests { AbsolutePathBuf::from_absolute_path(path).expect("path must be absolute") } + #[test] + fn command_execution_request_approval_rejects_relative_additional_permission_paths() { + let err = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "command": "cat file", + "cwd": "/tmp", + "commandActions": null, + "reason": null, + "networkApprovalContext": null, + "additionalPermissions": { + "network": null, + "fileSystem": { + "read": ["relative/path"], + "write": null + }, + "macos": null + }, + "proposedExecpolicyAmendment": null, + "proposedNetworkPolicyAmendments": null, + "availableDecisions": null + })) + .expect_err("relative additional permission paths should fail"); + assert!( + err.to_string() + .contains("AbsolutePathBuf deserialized without a base path"), + "unexpected error: {err}" + ); + } + #[test] fn sandbox_policy_round_trips_external_sandbox_network_access() { let v2_policy = SandboxPolicy::ExternalSandbox { diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 1cba215e26..27203c6012 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -57,9 +57,6 @@ use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserMessageResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::SkillApprovalDecision; -use codex_app_server_protocol::SkillRequestApprovalParams; -use codex_app_server_protocol::SkillRequestApprovalResponse; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; @@ -171,6 +168,9 @@ enum CliCommand { }, /// Send a user message through the app-server V2 thread/turn APIs. SendMessageV2 { + /// Opt into experimental app-server methods and fields. + #[arg(long)] + experimental_api: bool, /// User message to send to Codex. user_message: String, }, @@ -260,9 +260,18 @@ pub fn run() -> Result<()> { let endpoint = resolve_endpoint(codex_bin, url)?; send_message(&endpoint, &config_overrides, user_message) } - CliCommand::SendMessageV2 { user_message } => { + CliCommand::SendMessageV2 { + experimental_api, + user_message, + } => { let endpoint = resolve_endpoint(codex_bin, url)?; - send_message_v2_endpoint(&endpoint, &config_overrides, user_message, &dynamic_tools) + send_message_v2_endpoint( + &endpoint, + &config_overrides, + user_message, + experimental_api, + &dynamic_tools, + ) } CliCommand::ResumeMessageV2 { thread_id, @@ -508,19 +517,31 @@ pub fn send_message_v2( dynamic_tools: &Option>, ) -> Result<()> { let endpoint = Endpoint::SpawnCodex(codex_bin.to_path_buf()); - send_message_v2_endpoint(&endpoint, config_overrides, user_message, dynamic_tools) + send_message_v2_endpoint( + &endpoint, + config_overrides, + user_message, + true, + dynamic_tools, + ) } fn send_message_v2_endpoint( endpoint: &Endpoint, config_overrides: &[String], user_message: String, + experimental_api: bool, dynamic_tools: &Option>, ) -> Result<()> { + if dynamic_tools.is_some() && !experimental_api { + bail!("--dynamic-tools requires --experimental-api for send-message-v2"); + } + send_message_v2_with_policies( endpoint, config_overrides, user_message, + experimental_api, None, None, dynamic_tools, @@ -690,6 +711,7 @@ fn trigger_cmd_approval( endpoint, config_overrides, message, + true, Some(AskForApproval::OnRequest), Some(SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, @@ -711,6 +733,7 @@ fn trigger_patch_approval( endpoint, config_overrides, message, + true, Some(AskForApproval::OnRequest), Some(SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, @@ -729,6 +752,7 @@ fn no_trigger_cmd_approval( endpoint, config_overrides, prompt.to_string(), + true, None, None, dynamic_tools, @@ -739,13 +763,14 @@ fn send_message_v2_with_policies( endpoint: &Endpoint, config_overrides: &[String], user_message: String, + experimental_api: bool, approval_policy: Option, sandbox_policy: Option, dynamic_tools: &Option>, ) -> Result<()> { let mut client = CodexClient::connect(endpoint, config_overrides)?; - let initialize = client.initialize()?; + let initialize = client.initialize_with_experimental_api(experimental_api)?; println!("< initialize response: {initialize:?}"); let thread_response = client.thread_start(ThreadStartParams { @@ -888,6 +913,7 @@ fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u32) -> source_kinds: None, archived: None, cwd: None, + search_term: None, })?; println!("< thread/list response: {response:?}"); @@ -1032,6 +1058,13 @@ impl CodexClient { } fn initialize(&mut self) -> Result { + self.initialize_with_experimental_api(true) + } + + fn initialize_with_experimental_api( + &mut self, + experimental_api: bool, + ) -> Result { let request_id = self.request_id(); let request = ClientRequest::Initialize { request_id: request_id.clone(), @@ -1042,7 +1075,7 @@ impl CodexClient { version: env!("CARGO_PKG_VERSION").to_string(), }, capabilities: Some(InitializeCapabilities { - experimental_api: true, + experimental_api, opt_out_notification_methods: Some( NOTIFICATIONS_TO_OPT_OUT .iter() @@ -1475,9 +1508,6 @@ impl CodexClient { ServerRequest::FileChangeRequestApproval { request_id, params } => { self.approve_file_change_request(request_id, params)?; } - ServerRequest::SkillRequestApproval { request_id, params } => { - self.approve_skill_request(request_id, params)?; - } other => { bail!("received unsupported server request: {other:?}"); } @@ -1501,7 +1531,10 @@ impl CodexClient { command, cwd, command_actions, + additional_permissions, proposed_execpolicy_amendment, + proposed_network_policy_amendments, + available_decisions, } = params; println!( @@ -1516,6 +1549,9 @@ impl CodexClient { if let Some(network_approval_context) = network_approval_context.as_ref() { println!("< network approval context: {network_approval_context:?}"); } + if let Some(available_decisions) = available_decisions.as_ref() { + println!("< available decisions: {available_decisions:?}"); + } if let Some(command) = command.as_deref() { println!("< command: {command}"); } @@ -1527,9 +1563,15 @@ impl CodexClient { { println!("< command actions: {command_actions:?}"); } + if let Some(additional_permissions) = additional_permissions.as_ref() { + println!("< additional permissions: {additional_permissions:?}"); + } if let Some(execpolicy_amendment) = proposed_execpolicy_amendment.as_ref() { println!("< proposed execpolicy amendment: {execpolicy_amendment:?}"); } + if let Some(network_policy_amendments) = proposed_network_policy_amendments.as_ref() { + println!("< proposed network policy amendments: {network_policy_amendments:?}"); + } let decision = match self.command_approval_behavior { CommandApprovalBehavior::AlwaysAccept => CommandExecutionApprovalDecision::Accept, @@ -1549,22 +1591,6 @@ impl CodexClient { Ok(()) } - fn approve_skill_request( - &mut self, - request_id: RequestId, - params: SkillRequestApprovalParams, - ) -> Result<()> { - println!( - "\n< skill approval requested for item {}, skill {}", - params.item_id, params.skill_name - ); - let response = SkillRequestApprovalResponse { - decision: SkillApprovalDecision::Approve, - }; - self.send_server_request_response(request_id, &response)?; - Ok(()) - } - fn approve_file_change_request( &mut self, request_id: RequestId, diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 2f273a6794..eebaaba107 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -65,6 +65,7 @@ axum = { workspace = true, default-features = false, features = [ base64 = { workspace = true } codex-execpolicy = { workspace = true } core_test_support = { workspace = true } +codex-state = { workspace = true } codex-utils-cargo-bin = { workspace = true } os_info = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 8ee15688c6..1e5c37d286 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -122,11 +122,12 @@ Example with notification opt-out: - `thread/start` — create a new thread; emits `thread/started` and auto-subscribes you to turn/item events for that thread. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; emits `thread/started` and auto-subscribes you to turn/item events for the new thread. -- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, and `cwd` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. +- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`). - `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`. +- `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server shuts down and unloads the thread, then emits `thread/closed`. - `thread/name/set` — set or update a thread’s user-facing name; returns `{}` on success. Thread names are not required to be unique; name lookups resolve to the most recently updated thread. - `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`. - `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications. @@ -135,11 +136,15 @@ Example with notification opt-out: - `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. +- `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications. +- `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. +- `thread/realtime/appendText` — append text input to the active realtime session (experimental); returns `{}`. +- `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). -- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids. +- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. -- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). +- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**). - `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**). @@ -153,6 +158,8 @@ Example with notification opt-out: - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). - `config/read` — fetch the effective config on disk after resolving config layering. +- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home). +- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home). - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk. - `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints. @@ -170,6 +177,7 @@ Start a fresh thread when you need a new Codex conversation. "approvalPolicy": "never", "sandbox": "workspaceWrite", "personality": "friendly", + "serviceName": "my_app_server_client", // optional metrics tag (`service_name`) // Experimental: requires opt-in "dynamicTools": [ { @@ -229,6 +237,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per - `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`). - `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default). - `cwd` — restrict results to threads whose session cwd exactly matches this path. +- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive). - Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. Example: @@ -275,6 +284,26 @@ When `nextCursor` is `null`, you’ve reached the final page. } } ``` +### Example: Unsubscribe from a loaded thread + +`thread/unsubscribe` removes the current connection's subscription to a thread. The response status is one of: + +- `unsubscribed` when the connection was subscribed and is now removed. +- `notSubscribed` when the connection was not subscribed to that thread. +- `notLoaded` when the thread is not loaded. + +If this was the last subscriber, the server unloads the thread and emits `thread/closed` and a `thread/status/changed` transition to `notLoaded`. + +```json +{ "method": "thread/unsubscribe", "id": 22, "params": { "threadId": "thr_123" } } +{ "id": 22, "result": { "status": "unsubscribed" } } +{ "method": "thread/status/changed", "params": { + "threadId": "thr_123", + "status": { "type": "notLoaded" } +} } +{ "method": "thread/closed", "params": { "threadId": "thr_123" } } +``` + ### Example: Read a thread Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. @@ -547,7 +576,9 @@ Notes: ## Events -Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `turn/*`, and `item/*` notifications. +Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `thread/closed`, `turn/*`, and `item/*` notifications. + +Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`. ### Notification opt-out @@ -570,6 +601,18 @@ The fuzzy file search session API emits per-query notifications: - `fuzzyFileSearch/sessionUpdated` — `{ sessionId, query, files }` with the current matching files for the active query. - `fuzzyFileSearch/sessionCompleted` — `{ sessionId, query }` once indexing/matching for that query has completed. +### Thread realtime events (experimental) + +The thread realtime API emits thread-scoped notifications for session lifecycle and streaming media: + +- `thread/realtime/started` — `{ threadId, sessionId }` once realtime starts for the thread (experimental). +- `thread/realtime/itemAdded` — `{ threadId, item }` for non-audio realtime items (experimental). `item` is forwarded as raw JSON while the upstream websocket item schema remains unstable. +- `thread/realtime/outputAudio/delta` — `{ threadId, audio }` for streamed output audio chunks (experimental). `audio` uses camelCase fields (`data`, `sampleRate`, `numChannels`, `samplesPerChannel`). +- `thread/realtime/error` — `{ threadId, message }` when realtime encounters a transport or backend error (experimental). +- `thread/realtime/closed` — `{ threadId, reason }` when the realtime transport closes (experimental). + +Because audio is intentionally separate from `ThreadItem`, clients can opt out of `thread/realtime/outputAudio/delta` independently with `optOutNotificationMethods`. + ### Windows sandbox setup events - `windowsSandbox/setupCompleted` — `{ mode, success, error }` after a `windowsSandbox/setupStart` request finishes. @@ -660,15 +703,15 @@ When an upstream HTTP status is available (for example, from the Responses API o Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When `turn/start` is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing. - Requests include `threadId` and `turnId`—use them to scope UI state to the active conversation. -- Respond with a single `{ "decision": "accept" | "decline" }` payload (plus optional `acceptSettings` on command executions). The server resumes or declines the work and ends the item with `item/completed`. +- Respond with a single `{ "decision": ... }` payload. Command approvals support `accept`, `acceptForSession`, `acceptWithExecpolicyAmendment`, `applyNetworkPolicyAmendment`, `decline`, or `cancel`. The server resumes or declines the work and ends the item with `item/completed`. ### Command execution approvals Order of messages: 1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action. -2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. -3. Client response — `{ "decision": "accept", "acceptSettings": { "forSession": false } }` or `{ "decision": "decline" }`. +2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. +3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`. 4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result. ### File change approvals @@ -702,6 +745,13 @@ When a dynamic tool is invoked during a turn, the server sends an `item/tool/cal } ``` +The server also emits item lifecycle notifications around the request: + +1. `item/started` with `item.type = "dynamicToolCall"`, `status = "inProgress"`, plus `tool` and `arguments`. +2. `item/tool/call` request. +3. Client response. +4. `item/completed` with `item.type = "dynamicToolCall"`, final `status`, and the returned `contentItems`/`success`. + The client must respond with content items. Use `inputText` for text and `inputImage` for image URLs/data URLs: ```json @@ -1072,6 +1122,8 @@ At runtime, clients must send `initialize` with `capabilities.experimentalApi = 3. In `app-server-protocol/src/protocol/common.rs`, keep the method stable and use `inspect_params: true` when only some fields are experimental (like `thread/start`). If the entire method is experimental, annotate the method variant with `#[experimental("method/name")]`. +For server-initiated request payloads, annotate the field the same way so schema generation treats it as experimental, and make sure app-server omits that field when the client did not opt into `experimentalApi`. + 4. Regenerate protocol fixtures: ```bash diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 2a5557b5ba..0bc2f1118f 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -11,6 +11,7 @@ use crate::thread_state::TurnSummary; use crate::thread_status::ThreadWatchActiveGuard; use crate::thread_status::ThreadWatchManager; use codex_app_server_protocol::AccountRateLimitsUpdatedNotification; +use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile; use codex_app_server_protocol::AgentMessageDeltaNotification; use codex_app_server_protocol::ApplyPatchApprovalParams; use codex_app_server_protocol::ApplyPatchApprovalResponse; @@ -26,7 +27,9 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::ContextCompactedNotification; use codex_app_server_protocol::DeprecationNoticeNotification; +use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallParams; +use codex_app_server_protocol::DynamicToolCallStatus; use codex_app_server_protocol::ErrorNotification; use codex_app_server_protocol::ExecCommandApprovalParams; use codex_app_server_protocol::ExecCommandApprovalResponse; @@ -45,6 +48,8 @@ use codex_app_server_protocol::McpToolCallResult; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::ModelReroutedNotification; use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext; +use codex_app_server_protocol::NetworkPolicyAmendment as V2NetworkPolicyAmendment; +use codex_app_server_protocol::NetworkPolicyRuleAction as V2NetworkPolicyRuleAction; use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PlanDeltaNotification; use codex_app_server_protocol::RawResponseItemCompletedNotification; @@ -53,12 +58,14 @@ use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::ReasoningTextDeltaNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; -use codex_app_server_protocol::SkillApprovalDecision as V2SkillApprovalDecision; -use codex_app_server_protocol::SkillRequestApprovalParams; -use codex_app_server_protocol::SkillRequestApprovalResponse; use codex_app_server_protocol::TerminalInteractionNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadNameUpdatedNotification; +use codex_app_server_protocol::ThreadRealtimeClosedNotification; +use codex_app_server_protocol::ThreadRealtimeErrorNotification; +use codex_app_server_protocol::ThreadRealtimeItemAddedNotification; +use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification; +use codex_app_server_protocol::ThreadRealtimeStartedNotification; use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification; @@ -94,13 +101,13 @@ use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::McpToolCallBeginEvent; use codex_protocol::protocol::McpToolCallEndEvent; use codex_protocol::protocol::Op; +use codex_protocol::protocol::RealtimeEvent; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewOutputEvent; use codex_protocol::protocol::TokenCountEvent; use codex_protocol::protocol::TurnDiffEvent; use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; -use codex_protocol::skill_approval::SkillApprovalResponse as CoreSkillApprovalResponse; use codex_shell_command::parse_command::shlex_join; use std::collections::HashMap; use std::convert::TryFrom; @@ -170,6 +177,73 @@ pub(crate) async fn apply_bespoke_event_handling( .await; } } + EventMsg::RealtimeConversationStarted(event) => { + if let ApiVersion::V2 = api_version { + let notification = ThreadRealtimeStartedNotification { + thread_id: conversation_id.to_string(), + session_id: event.session_id, + }; + outgoing + .send_server_notification(ServerNotification::ThreadRealtimeStarted( + notification, + )) + .await; + } + } + EventMsg::RealtimeConversationRealtime(event) => { + if let ApiVersion::V2 = api_version { + match event.payload { + RealtimeEvent::SessionCreated { .. } => {} + RealtimeEvent::SessionUpdated { .. } => {} + RealtimeEvent::AudioOut(audio) => { + let notification = ThreadRealtimeOutputAudioDeltaNotification { + thread_id: conversation_id.to_string(), + audio: audio.into(), + }; + outgoing + .send_server_notification( + ServerNotification::ThreadRealtimeOutputAudioDelta(notification), + ) + .await; + } + RealtimeEvent::ConversationItemAdded(item) => { + let notification = ThreadRealtimeItemAddedNotification { + thread_id: conversation_id.to_string(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ThreadRealtimeItemAdded( + notification, + )) + .await; + } + RealtimeEvent::Error(message) => { + let notification = ThreadRealtimeErrorNotification { + thread_id: conversation_id.to_string(), + message, + }; + outgoing + .send_server_notification(ServerNotification::ThreadRealtimeError( + notification, + )) + .await; + } + } + } + } + EventMsg::RealtimeConversationClosed(event) => { + if let ApiVersion::V2 = api_version { + let notification = ThreadRealtimeClosedNotification { + thread_id: conversation_id.to_string(), + reason: event.reason, + }; + outgoing + .send_server_notification(ServerNotification::ThreadRealtimeClosed( + notification, + )) + .await; + } + } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { call_id, turn_id, @@ -258,6 +332,11 @@ pub(crate) async fn apply_bespoke_event_handling( .note_permission_requested(&conversation_id.to_string()) .await; let approval_id_for_op = ev.effective_approval_id(); + let available_decisions = ev + .effective_available_decisions() + .into_iter() + .map(CommandExecutionApprovalDecision::from) + .collect::>(); let ExecApprovalRequestEvent { call_id, approval_id, @@ -267,6 +346,8 @@ pub(crate) async fn apply_bespoke_event_handling( reason, network_approval_context, proposed_execpolicy_amendment, + proposed_network_policy_amendments, + additional_permissions, parsed_cmd, .. } = ev; @@ -329,6 +410,15 @@ pub(crate) async fn apply_bespoke_event_handling( }; let proposed_execpolicy_amendment_v2 = proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from); + let proposed_network_policy_amendments_v2 = proposed_network_policy_amendments + .map(|amendments| { + amendments + .into_iter() + .map(V2NetworkPolicyAmendment::from) + .collect() + }); + let additional_permissions = + additional_permissions.map(V2AdditionalPermissionProfile::from); let params = CommandExecutionRequestApprovalParams { thread_id: conversation_id.to_string(), @@ -340,7 +430,10 @@ pub(crate) async fn apply_bespoke_event_handling( command, cwd, command_actions, + additional_permissions, proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2, + proposed_network_policy_amendments: proposed_network_policy_amendments_v2, + available_decisions: Some(available_decisions), }; let rx = outgoing .send_request(ServerRequestPayload::CommandExecutionRequestApproval( @@ -427,46 +520,35 @@ pub(crate) async fn apply_bespoke_event_handling( } } } - EventMsg::SkillRequestApproval(request) => { - if matches!(api_version, ApiVersion::V2) { - let item_id = request.item_id; - let skill_name = request.skill_name; - let params = SkillRequestApprovalParams { - item_id: item_id.clone(), - skill_name, - }; - let rx = outgoing - .send_request(ServerRequestPayload::SkillRequestApproval(params)) - .await; - tokio::spawn(async move { - let approved = match rx.await { - Ok(Ok(value)) => { - serde_json::from_value::(value) - .map(|response| { - matches!(response.decision, V2SkillApprovalDecision::Approve) - }) - .unwrap_or(false) - } - _ => false, - }; - let _ = conversation - .submit(Op::SkillApproval { - id: item_id, - response: CoreSkillApprovalResponse { approved }, - }) - .await; - }); - } - } EventMsg::DynamicToolCallRequest(request) => { if matches!(api_version, ApiVersion::V2) { let call_id = request.call_id; + let turn_id = request.turn_id; + let tool = request.tool; + let arguments = request.arguments; + let item = ThreadItem::DynamicToolCall { + id: call_id.clone(), + tool: tool.clone(), + arguments: arguments.clone(), + status: DynamicToolCallStatus::InProgress, + content_items: None, + success: None, + duration_ms: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; let params = DynamicToolCallParams { thread_id: conversation_id.to_string(), - turn_id: request.turn_id, + turn_id: turn_id.clone(), call_id: call_id.clone(), - tool: request.tool, - arguments: request.arguments, + tool: tool.clone(), + arguments: arguments.clone(), }; let rx = outgoing .send_request(ServerRequestPayload::DynamicToolCall(params)) @@ -493,6 +575,46 @@ pub(crate) async fn apply_bespoke_event_handling( .await; } } + EventMsg::DynamicToolCallResponse(response) => { + if matches!(api_version, ApiVersion::V2) { + let status = if response.success { + DynamicToolCallStatus::Completed + } else { + DynamicToolCallStatus::Failed + }; + let duration_ms = i64::try_from(response.duration.as_millis()).ok(); + let item = ThreadItem::DynamicToolCall { + id: response.call_id, + tool: response.tool, + arguments: response.arguments, + status, + content_items: Some( + response + .content_items + .into_iter() + .map(|item| match item { + CoreDynamicToolCallOutputContentItem::InputText { text } => { + DynamicToolCallOutputContentItem::InputText { text } + } + CoreDynamicToolCallOutputContentItem::InputImage { image_url } => { + DynamicToolCallOutputContentItem::InputImage { image_url } + } + }) + .collect(), + ), + success: Some(response.success), + duration_ms, + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: response.turn_id, + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } + } // TODO(celia): properly construct McpToolCall TurnItem in core. EventMsg::McpToolCallBegin(begin_event) => { let notification = construct_mcp_tool_call_notification( @@ -1910,6 +2032,20 @@ async fn on_command_execution_request_approval_response( }, None, ), + CommandExecutionApprovalDecision::ApplyNetworkPolicyAmendment { + network_policy_amendment, + } => { + let completion_status = match network_policy_amendment.action { + V2NetworkPolicyRuleAction::Allow => None, + V2NetworkPolicyRuleAction::Deny => Some(CommandExecutionStatus::Declined), + }; + ( + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.into_core(), + }, + completion_status, + ) + } CommandExecutionApprovalDecision::Decline => ( ReviewDecision::Denied, Some(CommandExecutionStatus::Declined), @@ -2225,7 +2361,11 @@ mod tests { let event_turn_id = "complete1".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); let thread_state = new_thread_state(); handle_turn_complete( @@ -2266,7 +2406,11 @@ mod tests { .await; let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); handle_turn_interrupted( conversation_id, @@ -2306,7 +2450,11 @@ mod tests { .await; let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); handle_turn_complete( conversation_id, @@ -2340,7 +2488,11 @@ mod tests { async fn test_handle_turn_plan_update_emits_notification_for_v2() -> Result<()> { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); let update = UpdatePlanArgs { explanation: Some("need plan".to_string()), plan: vec![ @@ -2390,7 +2542,11 @@ mod tests { let turn_id = "turn-123".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); let info = TokenUsageInfo { total_token_usage: TokenUsage { @@ -2474,7 +2630,11 @@ mod tests { let turn_id = "turn-456".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); handle_token_count_event( conversation_id, @@ -2541,7 +2701,11 @@ mod tests { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); // Turn 1 on conversation A let a_turn1 = "a_turn1".to_string(); @@ -2764,7 +2928,11 @@ mod tests { async fn test_handle_turn_diff_emits_v2_notification() -> Result<()> { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); let unified_diff = "--- a\n+++ b\n".to_string(); let conversation_id = ThreadId::new(); @@ -2798,7 +2966,11 @@ mod tests { async fn test_handle_turn_diff_is_noop_for_v1() -> Result<()> { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); - let outgoing = ThreadScopedOutgoingMessageSender::new(outgoing, vec![ConnectionId(1)]); + let outgoing = ThreadScopedOutgoingMessageSender::new( + outgoing, + vec![ConnectionId(1)], + ThreadId::new(), + ); let conversation_id = ThreadId::new(); handle_turn_diff( diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 276b4ff4ad..1f6e1ac0f1 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1,5 +1,7 @@ use crate::bespoke_event_handling::apply_bespoke_event_handling; +use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE; use crate::error_code::INTERNAL_ERROR_CODE; +use crate::error_code::INVALID_PARAMS_ERROR_CODE; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::fuzzy_file_search::FuzzyFileSearchSession; use crate::fuzzy_file_search::run_fuzzy_file_search; @@ -127,6 +129,7 @@ use codex_app_server_protocol::ThreadArchiveResponse; use codex_app_server_protocol::ThreadArchivedNotification; use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams; use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse; +use codex_app_server_protocol::ThreadClosedNotification; use codex_app_server_protocol::ThreadCompactStartParams; use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadForkParams; @@ -138,6 +141,14 @@ use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadLoadedListResponse; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadRealtimeAppendAudioParams; +use codex_app_server_protocol::ThreadRealtimeAppendAudioResponse; +use codex_app_server_protocol::ThreadRealtimeAppendTextParams; +use codex_app_server_protocol::ThreadRealtimeAppendTextResponse; +use codex_app_server_protocol::ThreadRealtimeStartParams; +use codex_app_server_protocol::ThreadRealtimeStartResponse; +use codex_app_server_protocol::ThreadRealtimeStopParams; +use codex_app_server_protocol::ThreadRealtimeStopResponse; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadRollbackParams; @@ -152,6 +163,9 @@ use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; use codex_app_server_protocol::ThreadUnarchivedNotification; +use codex_app_server_protocol::ThreadUnsubscribeParams; +use codex_app_server_protocol::ThreadUnsubscribeResponse; +use codex_app_server_protocol::ThreadUnsubscribeStatus; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnStartParams; @@ -168,6 +182,7 @@ use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_app_server_protocol::WindowsSandboxSetupStartParams; use codex_app_server_protocol::WindowsSandboxSetupStartResponse; use codex_app_server_protocol::build_turns_from_rollout_items; +use codex_arg0::Arg0DispatchPaths; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_cloud_requirements::cloud_requirements_loader; @@ -189,6 +204,7 @@ use codex_core::auth::login_with_chatgpt_auth_tokens; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigService; +use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::types::McpServerTransportConfig; @@ -208,6 +224,7 @@ use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; @@ -233,6 +250,9 @@ use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AgentStatus; +use codex_protocol::protocol::ConversationAudioParams; +use codex_protocol::protocol::ConversationStartParams; +use codex_protocol::protocol::ConversationTextParams; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GitInfo as CoreGitInfo; use codex_protocol::protocol::InitialHistory; @@ -249,6 +269,7 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::USER_MESSAGE_BEGIN; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::UserInput as CoreInputItem; use codex_rmcp_client::perform_oauth_login_return_url; use codex_utils_json_to_toml::json_to_toml; @@ -289,6 +310,7 @@ struct ThreadListFilters { source_kinds: Option>, archived: bool, cwd: Option, + search_term: Option, } // Duration before a ChatGPT login attempt is abandoned. @@ -309,6 +331,12 @@ enum AppListLoadResult { Directory(Result, String>), } +enum ThreadShutdownResult { + Complete, + SubmitFailed, + TimedOut, +} + fn convert_remote_scope(scope: ApiHazelnutScope) -> RemoteSkillHazelnutScope { match scope { ApiHazelnutScope::WorkspaceShared => RemoteSkillHazelnutScope::WorkspaceShared, @@ -338,12 +366,13 @@ pub(crate) struct CodexMessageProcessor { auth_manager: Arc, thread_manager: Arc, outgoing: Arc, - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, config: Arc, single_client_mode: bool, cli_overrides: Vec<(String, TomlValue)>, cloud_requirements: Arc>, active_login: Arc>>, + pending_thread_unloads: Arc>>, thread_state_manager: ThreadStateManager, thread_watch_manager: ThreadWatchManager, pending_fuzzy_searches: Arc>>>, @@ -362,7 +391,7 @@ pub(crate) struct CodexMessageProcessorArgs { pub(crate) auth_manager: Arc, pub(crate) thread_manager: Arc, pub(crate) outgoing: Arc, - pub(crate) codex_linux_sandbox_exe: Option, + pub(crate) arg0_paths: Arg0DispatchPaths, pub(crate) config: Arc, pub(crate) cli_overrides: Vec<(String, TomlValue)>, pub(crate) cloud_requirements: Arc>, @@ -399,7 +428,7 @@ impl CodexMessageProcessor { auth_manager, thread_manager, outgoing, - codex_linux_sandbox_exe, + arg0_paths, config, cli_overrides, cloud_requirements, @@ -410,12 +439,13 @@ impl CodexMessageProcessor { auth_manager, thread_manager, outgoing: outgoing.clone(), - codex_linux_sandbox_exe, + arg0_paths, config, single_client_mode, cli_overrides, cloud_requirements, active_login: Arc::new(Mutex::new(None)), + pending_thread_unloads: Arc::new(Mutex::new(HashSet::new())), thread_state_manager: ThreadStateManager::new(), thread_watch_manager: ThreadWatchManager::new_with_outgoing(outgoing), pending_fuzzy_searches: Arc::new(Mutex::new(HashMap::new())), @@ -426,7 +456,7 @@ impl CodexMessageProcessor { async fn load_latest_config(&self) -> Result { let cloud_requirements = self.current_cloud_requirements(); - codex_core::config::ConfigBuilder::default() + let mut config = codex_core::config::ConfigBuilder::default() .cli_overrides(self.cli_overrides.clone()) .cloud_requirements(cloud_requirements) .build() @@ -435,7 +465,10 @@ impl CodexMessageProcessor { code: INTERNAL_ERROR_CODE, message: format!("failed to reload config: {err}"), data: None, - }) + })?; + config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); + config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone(); + Ok(config) } fn current_cloud_requirements(&self) -> CloudRequirementsLoader { @@ -450,11 +483,13 @@ impl CodexMessageProcessor { fn normalize_turn_start_collaboration_mode( &self, mut collaboration_mode: CollaborationMode, + collaboration_modes_config: CollaborationModesConfig, ) -> CollaborationMode { if collaboration_mode.settings.developer_instructions.is_none() && let Some(instructions) = self .thread_manager - .list_collaboration_modes() + .get_models_manager() + .list_collaboration_modes_for_config(collaboration_modes_config) .into_iter() .find(|preset| preset.mode == Some(collaboration_mode.mode)) .and_then(|preset| preset.developer_instructions.flatten()) @@ -525,7 +560,12 @@ impl CodexMessageProcessor { Ok((review_request, hint)) } - pub async fn process_request(&mut self, connection_id: ConnectionId, request: ClientRequest) { + pub async fn process_request( + &mut self, + connection_id: ConnectionId, + request: ClientRequest, + app_server_client_name: Option, + ) { let to_connection_request_id = |request_id| ConnectionRequestId { connection_id, request_id, @@ -540,6 +580,10 @@ impl CodexMessageProcessor { self.thread_start(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadUnsubscribe { request_id, params } => { + self.thread_unsubscribe(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadResume { request_id, params } => { self.thread_resume(to_connection_request_id(request_id), params) .await; @@ -608,8 +652,12 @@ impl CodexMessageProcessor { .await; } ClientRequest::TurnStart { request_id, params } => { - self.turn_start(to_connection_request_id(request_id), params) - .await; + self.turn_start( + to_connection_request_id(request_id), + params, + app_server_client_name.clone(), + ) + .await; } ClientRequest::TurnSteer { request_id, params } => { self.turn_steer(to_connection_request_id(request_id), params) @@ -619,6 +667,22 @@ impl CodexMessageProcessor { self.turn_interrupt(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadRealtimeStart { request_id, params } => { + self.thread_realtime_start(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadRealtimeAppendAudio { request_id, params } => { + self.thread_realtime_append_audio(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadRealtimeAppendText { request_id, params } => { + self.thread_realtime_append_text(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadRealtimeStop { request_id, params } => { + self.thread_realtime_stop(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ReviewStart { request_id, params } => { self.review_start(to_connection_request_id(request_id), params) .await; @@ -712,12 +776,20 @@ impl CodexMessageProcessor { .await; } ClientRequest::SendUserMessage { request_id, params } => { - self.send_user_message(to_connection_request_id(request_id), params) - .await; + self.send_user_message( + to_connection_request_id(request_id), + params, + app_server_client_name.clone(), + ) + .await; } ClientRequest::SendUserTurn { request_id, params } => { - self.send_user_turn(to_connection_request_id(request_id), params) - .await; + self.send_user_turn( + to_connection_request_id(request_id), + params, + app_server_client_name.clone(), + ) + .await; } ClientRequest::InterruptConversation { request_id, params } => { self.interrupt_conversation(to_connection_request_id(request_id), params) @@ -817,6 +889,10 @@ impl CodexMessageProcessor { ClientRequest::ConfigRequirementsRead { .. } => { warn!("ConfigRequirementsRead request reached CodexMessageProcessor unexpectedly"); } + ClientRequest::ExternalAgentConfigDetect { .. } + | ClientRequest::ExternalAgentConfigImport { .. } => { + warn!("ExternalAgentConfig request reached CodexMessageProcessor unexpectedly"); + } ClientRequest::GetAccountRateLimits { request_id, params: _, @@ -1742,6 +1818,7 @@ impl CodexMessageProcessor { None, None, managed_network_requirements_enabled, + NetworkProxyAuditMetadata::default(), ) .await { @@ -1759,10 +1836,8 @@ impl CodexMessageProcessor { None => None, }; let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); - let command = params.command; let exec_params = ExecParams { - original_command: command.join(" "), - command, + command: params.command, cwd, expiration: timeout_ms.into(), env, @@ -1792,7 +1867,7 @@ impl CodexMessageProcessor { None => self.config.permissions.sandbox_policy.get().clone(), }; - let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone(); + let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); let outgoing = self.outgoing.clone(); let request_for_task = request; let sandbox_cwd = self.config.cwd.clone(); @@ -1857,7 +1932,8 @@ impl CodexMessageProcessor { approval_policy, sandbox_mode, model_provider, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, compact_prompt, @@ -1951,6 +2027,7 @@ impl CodexMessageProcessor { approval_policy, sandbox, config, + service_name, base_instructions, developer_instructions, dynamic_tools, @@ -2018,7 +2095,12 @@ impl CodexMessageProcessor { match self .thread_manager - .start_thread_with_tools(config, core_dynamic_tools, persist_extended_history) + .start_thread_with_tools_and_service_name( + config, + core_dynamic_tools, + persist_extended_history, + service_name, + ) .await { Ok(new_conv) => { @@ -2111,7 +2193,8 @@ impl CodexMessageProcessor { approval_policy: approval_policy .map(codex_app_server_protocol::AskForApproval::to_core), sandbox_mode: sandbox.map(SandboxMode::to_core), - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, personality, @@ -2525,6 +2608,7 @@ impl CodexMessageProcessor { source_kinds, archived, cwd, + search_term, } = params; let requested_page_size = limit @@ -2545,6 +2629,7 @@ impl CodexMessageProcessor { source_kinds, archived: archived.unwrap_or(false), cwd: cwd.map(PathBuf::from), + search_term, }, ) .await @@ -2828,6 +2913,23 @@ impl CodexMessageProcessor { } async fn thread_resume(&mut self, request_id: ConnectionRequestId, params: ThreadResumeParams) { + if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id) + && self + .pending_thread_unloads + .lock() + .await + .contains(&thread_id) + { + self.send_invalid_request_error( + request_id, + format!( + "thread {thread_id} is closing; retry thread/resume after the thread is closed" + ), + ) + .await; + return; + } + if self .resume_running_thread(request_id.clone(), ¶ms) .await @@ -3606,6 +3708,7 @@ impl CodexMessageProcessor { source_kinds: None, archived: false, cwd: None, + search_term: None, }, ) .await @@ -3632,6 +3735,7 @@ impl CodexMessageProcessor { source_kinds, archived, cwd, + search_term, } = filters; let mut cursor_obj: Option = match cursor.as_ref() { Some(cursor_str) => { @@ -3674,6 +3778,7 @@ impl CodexMessageProcessor { allowed_sources, model_provider_filter.as_deref(), fallback_provider.as_str(), + search_term.as_deref(), ) .await .map_err(|err| JSONRPCErrorError { @@ -3690,6 +3795,7 @@ impl CodexMessageProcessor { allowed_sources, model_provider_filter.as_deref(), fallback_provider.as_str(), + search_term.as_deref(), ) .await .map_err(|err| JSONRPCErrorError { @@ -3826,7 +3932,11 @@ impl CodexMessageProcessor { params: CollaborationModeListParams, ) { let CollaborationModeListParams {} = params; - let items = thread_manager.list_collaboration_modes(); + let items = thread_manager + .list_collaboration_modes() + .into_iter() + .map(Into::into) + .collect(); let response = CollaborationModeListResponse { data: items }; outgoing.send_response(request_id, response).await; } @@ -4059,6 +4169,7 @@ impl CodexMessageProcessor { http_headers, env_http_headers, scopes.as_deref().unwrap_or_default(), + server.oauth_resource.as_deref(), timeout_secs, config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), @@ -4325,7 +4436,8 @@ impl CodexMessageProcessor { approval_policy, sandbox_mode, model_provider, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, compact_prompt, @@ -4336,7 +4448,8 @@ impl CodexMessageProcessor { } None => ( ConfigOverrides { - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), ..Default::default() }, None, @@ -4520,7 +4633,8 @@ impl CodexMessageProcessor { approval_policy, sandbox_mode, model_provider, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, compact_prompt, @@ -4532,7 +4646,8 @@ impl CodexMessageProcessor { } None => ( ConfigOverrides { - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), + codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), ..Default::default() }, None, @@ -4641,6 +4756,36 @@ impl CodexMessageProcessor { self.outgoing.send_error(request_id, error).await; } + fn input_too_large_error(actual_chars: usize) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INVALID_PARAMS_ERROR_CODE, + message: format!( + "Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters." + ), + data: Some(serde_json::json!({ + "input_error_code": INPUT_TOO_LARGE_ERROR_CODE, + "max_chars": MAX_USER_INPUT_TEXT_CHARS, + "actual_chars": actual_chars, + })), + } + } + + fn validate_v1_input_limit(items: &[WireInputItem]) -> Result<(), JSONRPCErrorError> { + let actual_chars: usize = items.iter().map(WireInputItem::text_char_count).sum(); + if actual_chars > MAX_USER_INPUT_TEXT_CHARS { + return Err(Self::input_too_large_error(actual_chars)); + } + Ok(()) + } + + fn validate_v2_input_limit(items: &[V2UserInput]) -> Result<(), JSONRPCErrorError> { + let actual_chars: usize = items.iter().map(V2UserInput::text_char_count).sum(); + if actual_chars > MAX_USER_INPUT_TEXT_CHARS { + return Err(Self::input_too_large_error(actual_chars)); + } + Ok(()) + } + async fn send_internal_error(&self, request_id: ConnectionRequestId, message: String) { let error = JSONRPCErrorError { code: INTERNAL_ERROR_CODE, @@ -4673,6 +4818,150 @@ impl CodexMessageProcessor { } } + async fn wait_for_thread_shutdown(thread: &Arc) -> ThreadShutdownResult { + match thread.submit(Op::Shutdown).await { + Ok(_) => { + let wait_for_shutdown = async { + loop { + if matches!(thread.agent_status().await, AgentStatus::Shutdown) { + break; + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + }; + if tokio::time::timeout(Duration::from_secs(10), wait_for_shutdown) + .await + .is_err() + { + ThreadShutdownResult::TimedOut + } else { + ThreadShutdownResult::Complete + } + } + Err(_) => ThreadShutdownResult::SubmitFailed, + } + } + + async fn finalize_thread_teardown(&mut self, thread_id: ThreadId) { + self.pending_thread_unloads.lock().await.remove(&thread_id); + self.outgoing.cancel_requests_for_thread(thread_id).await; + self.thread_state_manager + .remove_thread_state(thread_id) + .await; + self.thread_watch_manager + .remove_thread(&thread_id.to_string()) + .await; + } + + async fn thread_unsubscribe( + &mut self, + request_id: ConnectionRequestId, + params: ThreadUnsubscribeParams, + ) { + let thread_id = match ThreadId::from_string(¶ms.thread_id) { + Ok(id) => id, + Err(err) => { + self.send_invalid_request_error(request_id, format!("invalid thread id: {err}")) + .await; + return; + } + }; + + let Ok(thread) = self.thread_manager.get_thread(thread_id).await else { + // Reconcile stale app-server bookkeeping when the thread has already been + // removed from the core manager. This keeps loaded-status/subscription state + // consistent with the source of truth before reporting NotLoaded. + self.finalize_thread_teardown(thread_id).await; + self.outgoing + .send_response( + request_id, + ThreadUnsubscribeResponse { + status: ThreadUnsubscribeStatus::NotLoaded, + }, + ) + .await; + return; + }; + + let was_subscribed = self + .thread_state_manager + .unsubscribe_connection_from_thread(thread_id, request_id.connection_id) + .await; + if !was_subscribed { + self.outgoing + .send_response( + request_id, + ThreadUnsubscribeResponse { + status: ThreadUnsubscribeStatus::NotSubscribed, + }, + ) + .await; + return; + } + + if !self.thread_state_manager.has_subscribers(thread_id).await { + // This connection was the last subscriber. Only now do we unload the thread. + info!("thread {thread_id} has no subscribers; shutting down"); + self.pending_thread_unloads.lock().await.insert(thread_id); + // Any pending app-server -> client requests for this thread can no longer be + // answered; cancel their callbacks before shutdown/unload. + self.outgoing.cancel_requests_for_thread(thread_id).await; + self.thread_state_manager + .remove_thread_state(thread_id) + .await; + + let outgoing = self.outgoing.clone(); + let pending_thread_unloads = self.pending_thread_unloads.clone(); + let thread_manager = self.thread_manager.clone(); + let thread_watch_manager = self.thread_watch_manager.clone(); + tokio::spawn(async move { + match Self::wait_for_thread_shutdown(&thread).await { + ThreadShutdownResult::Complete => { + if thread_manager.remove_thread(&thread_id).await.is_none() { + info!( + "thread {thread_id} was already removed before unsubscribe finalized" + ); + thread_watch_manager + .remove_thread(&thread_id.to_string()) + .await; + pending_thread_unloads.lock().await.remove(&thread_id); + return; + } + thread_watch_manager + .remove_thread(&thread_id.to_string()) + .await; + let notification = ThreadClosedNotification { + thread_id: thread_id.to_string(), + }; + outgoing + .send_server_notification(ServerNotification::ThreadClosed( + notification, + )) + .await; + pending_thread_unloads.lock().await.remove(&thread_id); + } + ThreadShutdownResult::SubmitFailed => { + pending_thread_unloads.lock().await.remove(&thread_id); + warn!("failed to submit Shutdown to thread {thread_id}"); + } + ThreadShutdownResult::TimedOut => { + pending_thread_unloads.lock().await.remove(&thread_id); + warn!("thread {thread_id} shutdown timed out; leaving thread loaded"); + } + } + }); + } + + self.outgoing + .send_response( + request_id, + ThreadUnsubscribeResponse { + status: ThreadUnsubscribeStatus::Unsubscribed, + }, + ) + .await; + } + async fn archive_thread_common( &mut self, thread_id: ThreadId, @@ -4744,37 +5033,19 @@ impl CodexMessageProcessor { state_db_ctx = Some(ctx); } info!("thread {thread_id} was active; shutting down"); - // Request shutdown. - match conversation.submit(Op::Shutdown).await { - Ok(_) => { - // Poll agent status rather than consuming events so attached listeners do not block shutdown. - let wait_for_shutdown = async { - loop { - if matches!(conversation.agent_status().await, AgentStatus::Shutdown) { - break; - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - }; - if tokio::time::timeout(Duration::from_secs(10), wait_for_shutdown) - .await - .is_err() - { - warn!("thread {thread_id} shutdown timed out; proceeding with archive"); - } + match Self::wait_for_thread_shutdown(&conversation).await { + ThreadShutdownResult::Complete => {} + ThreadShutdownResult::SubmitFailed => { + error!( + "failed to submit Shutdown to thread {thread_id}; proceeding with archive" + ); } - Err(err) => { - error!("failed to submit Shutdown to thread {thread_id}: {err}"); + ThreadShutdownResult::TimedOut => { + warn!("thread {thread_id} shutdown timed out; proceeding with archive"); } } - self.thread_state_manager - .remove_thread_state(thread_id) - .await; } - - self.thread_watch_manager - .remove_thread(&thread_id.to_string()) - .await; + self.finalize_thread_teardown(thread_id).await; if state_db_ctx.is_none() { state_db_ctx = get_state_db(&self.config, None).await; @@ -4809,11 +5080,16 @@ impl CodexMessageProcessor { &self, request_id: ConnectionRequestId, params: SendUserMessageParams, + app_server_client_name: Option, ) { let SendUserMessageParams { conversation_id, items, } = params; + if let Err(error) = Self::validate_v1_input_limit(&items) { + self.outgoing.send_error(request_id, error).await; + return; + } let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -4823,6 +5099,12 @@ impl CodexMessageProcessor { self.outgoing.send_error(request_id, error).await; return; }; + if let Err(error) = + Self::set_app_server_client_name(conversation.as_ref(), app_server_client_name).await + { + self.outgoing.send_error(request_id, error).await; + return; + } let mapped_items: Vec = items .into_iter() @@ -4853,7 +5135,12 @@ impl CodexMessageProcessor { .await; } - async fn send_user_turn(&self, request_id: ConnectionRequestId, params: SendUserTurnParams) { + async fn send_user_turn( + &self, + request_id: ConnectionRequestId, + params: SendUserTurnParams, + app_server_client_name: Option, + ) { let SendUserTurnParams { conversation_id, items, @@ -4865,6 +5152,10 @@ impl CodexMessageProcessor { summary, output_schema, } = params; + if let Err(error) = Self::validate_v1_input_limit(&items) { + self.outgoing.send_error(request_id, error).await; + return; + } let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await else { let error = JSONRPCErrorError { @@ -4875,6 +5166,12 @@ impl CodexMessageProcessor { self.outgoing.send_error(request_id, error).await; return; }; + if let Err(error) = + Self::set_app_server_client_name(conversation.as_ref(), app_server_client_name).await + { + self.outgoing.send_error(request_id, error).await; + return; + } let mapped_items: Vec = items .into_iter() @@ -4899,7 +5196,7 @@ impl CodexMessageProcessor { sandbox_policy, model, effort, - summary, + summary: Some(summary), final_output_json_schema: output_schema, collaboration_mode: None, personality: None, @@ -4988,6 +5285,7 @@ impl CodexMessageProcessor { connectors::list_cached_accessible_connectors_from_mcp_tools(&config), connectors::list_cached_all_connectors(&config) ); + let cached_all_connectors = all_connectors.clone(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); @@ -5014,6 +5312,19 @@ impl CodexMessageProcessor { let app_list_deadline = tokio::time::Instant::now() + APP_LIST_LOAD_TIMEOUT; let mut accessible_loaded = false; let mut all_loaded = false; + let mut last_notified_apps = None; + + if accessible_connectors.is_some() || all_connectors.is_some() { + let merged = connectors::with_app_enabled_state( + Self::merge_loaded_apps( + all_connectors.as_deref(), + accessible_connectors.as_deref(), + ), + &config, + ); + Self::send_app_list_updated_notification(&outgoing, merged.clone()).await; + last_notified_apps = Some(merged); + } loop { let result = match tokio::time::timeout_at(app_list_deadline, rx.recv()).await { @@ -5070,14 +5381,30 @@ impl CodexMessageProcessor { } } + let showing_interim_force_refetch = force_refetch && !(accessible_loaded && all_loaded); + let all_connectors_for_update = + if showing_interim_force_refetch && cached_all_connectors.is_some() { + cached_all_connectors.as_deref() + } else { + all_connectors.as_deref() + }; + let accessible_connectors_for_update = + if showing_interim_force_refetch && !accessible_loaded { + None + } else { + accessible_connectors.as_deref() + }; let merged = connectors::with_app_enabled_state( Self::merge_loaded_apps( - all_connectors.as_deref(), - accessible_connectors.as_deref(), + all_connectors_for_update, + accessible_connectors_for_update, ), &config, ); - Self::send_app_list_updated_notification(&outgoing, merged.clone()).await; + if last_notified_apps.as_ref() != Some(&merged) { + Self::send_app_list_updated_notification(&outgoing, merged.clone()).await; + last_notified_apps = Some(merged.clone()); + } if accessible_loaded && all_loaded { match Self::paginate_apps(merged.as_slice(), start, limit) { @@ -5346,7 +5673,16 @@ impl CodexMessageProcessor { let _ = conversation.submit(Op::Interrupt).await; } - async fn turn_start(&self, request_id: ConnectionRequestId, params: TurnStartParams) { + async fn turn_start( + &self, + request_id: ConnectionRequestId, + params: TurnStartParams, + app_server_client_name: Option, + ) { + if let Err(error) = Self::validate_v2_input_limit(¶ms.input) { + self.outgoing.send_error(request_id, error).await; + return; + } let (_, thread) = match self.load_thread(¶ms.thread_id).await { Ok(v) => v, Err(error) => { @@ -5354,10 +5690,19 @@ impl CodexMessageProcessor { return; } }; + if let Err(error) = + Self::set_app_server_client_name(thread.as_ref(), app_server_client_name).await + { + self.outgoing.send_error(request_id, error).await; + return; + } - let collaboration_mode = params - .collaboration_mode - .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); + let collaboration_modes_config = CollaborationModesConfig { + default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput), + }; + let collaboration_mode = params.collaboration_mode.map(|mode| { + self.normalize_turn_start_collaboration_mode(mode, collaboration_modes_config) + }); // Map v2 input items to core input items. let mapped_items: Vec = params @@ -5432,6 +5777,20 @@ impl CodexMessageProcessor { } } + async fn set_app_server_client_name( + thread: &CodexThread, + app_server_client_name: Option, + ) -> Result<(), JSONRPCErrorError> { + thread + .set_app_server_client_name(app_server_client_name) + .await + .map_err(|err| JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to set app server client name: {err}"), + data: None, + }) + } + async fn turn_steer(&self, request_id: ConnectionRequestId, params: TurnSteerParams) { let (_, thread) = match self.load_thread(¶ms.thread_id).await { Ok(v) => v, @@ -5449,6 +5808,10 @@ impl CodexMessageProcessor { .await; return; } + if let Err(error) = Self::validate_v2_input_limit(¶ms.input) { + self.outgoing.send_error(request_id, error).await; + return; + } let mapped_items: Vec = params .input @@ -5489,6 +5852,177 @@ impl CodexMessageProcessor { } } + async fn prepare_realtime_conversation_thread( + &mut self, + request_id: ConnectionRequestId, + thread_id: &str, + ) -> Option<(ThreadId, Arc)> { + let (thread_id, thread) = match self.load_thread(thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return None; + } + }; + + if let Err(error) = self + .ensure_conversation_listener( + thread_id, + request_id.connection_id, + false, + ApiVersion::V2, + ) + .await + { + self.outgoing.send_error(request_id, error).await; + return None; + } + + if !thread.enabled(Feature::RealtimeConversation) { + self.send_invalid_request_error( + request_id, + format!("thread {thread_id} does not support realtime conversation"), + ) + .await; + return None; + } + + Some((thread_id, thread)) + } + + async fn thread_realtime_start( + &mut self, + request_id: ConnectionRequestId, + params: ThreadRealtimeStartParams, + ) { + let Some((_, thread)) = self + .prepare_realtime_conversation_thread(request_id.clone(), ¶ms.thread_id) + .await + else { + return; + }; + + let submit = thread + .submit(Op::RealtimeConversationStart(ConversationStartParams { + prompt: params.prompt, + session_id: params.session_id, + })) + .await; + + match submit { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadRealtimeStartResponse::default()) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to start realtime conversation: {err}"), + ) + .await; + } + } + } + + async fn thread_realtime_append_audio( + &mut self, + request_id: ConnectionRequestId, + params: ThreadRealtimeAppendAudioParams, + ) { + let Some((_, thread)) = self + .prepare_realtime_conversation_thread(request_id.clone(), ¶ms.thread_id) + .await + else { + return; + }; + + let submit = thread + .submit(Op::RealtimeConversationAudio(ConversationAudioParams { + frame: params.audio.into(), + })) + .await; + + match submit { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadRealtimeAppendAudioResponse::default()) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to append realtime conversation audio: {err}"), + ) + .await; + } + } + } + + async fn thread_realtime_append_text( + &mut self, + request_id: ConnectionRequestId, + params: ThreadRealtimeAppendTextParams, + ) { + let Some((_, thread)) = self + .prepare_realtime_conversation_thread(request_id.clone(), ¶ms.thread_id) + .await + else { + return; + }; + + let submit = thread + .submit(Op::RealtimeConversationText(ConversationTextParams { + text: params.text, + })) + .await; + + match submit { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadRealtimeAppendTextResponse::default()) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to append realtime conversation text: {err}"), + ) + .await; + } + } + } + + async fn thread_realtime_stop( + &mut self, + request_id: ConnectionRequestId, + params: ThreadRealtimeStopParams, + ) { + let Some((_, thread)) = self + .prepare_realtime_conversation_thread(request_id.clone(), ¶ms.thread_id) + .await + else { + return; + }; + + let submit = thread.submit(Op::RealtimeConversationClose).await; + + match submit { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadRealtimeStopResponse::default()) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to stop realtime conversation: {err}"), + ) + .await; + } + } + } + fn build_review_turn(turn_id: String, display_text: &str) -> Turn { let items = if display_text.is_empty() { Vec::new() @@ -5953,6 +6487,7 @@ impl CodexMessageProcessor { let thread_outgoing = ThreadScopedOutgoingMessageSender::new( outgoing_for_task.clone(), subscribed_connection_ids, + conversation_id, ); apply_bespoke_event_handling( event.clone(), @@ -6255,10 +6790,8 @@ impl CodexMessageProcessor { WindowsSandboxSetupMode::Unelevated => CoreWindowsSandboxSetupMode::Unelevated, }; let config = Arc::clone(&self.config); - let outgoing = ThreadScopedOutgoingMessageSender::new( - Arc::clone(&self.outgoing), - vec![request_id.connection_id], - ); + let outgoing = Arc::clone(&self.outgoing); + let connection_id = request_id.connection_id; tokio::spawn(async move { let setup_request = WindowsSandboxSetupRequest { @@ -6281,9 +6814,10 @@ impl CodexMessageProcessor { error: setup_result.err().map(|err| err.to_string()), }; outgoing - .send_server_notification(ServerNotification::WindowsSandboxSetupCompleted( - notification, - )) + .send_server_notification_to_connections( + &[connection_id], + ServerNotification::WindowsSandboxSetupCompleted(notification), + ) .await; }); } @@ -7184,7 +7718,7 @@ mod tests { "role": "user", "content": [{ "type": "input_text", - "text": "\n\n".to_string(), + "text": "# AGENTS.md instructions for project\n\n\n\n".to_string(), }], }), json!({ diff --git a/codex-rs/app-server/src/dynamic_tools.rs b/codex-rs/app-server/src/dynamic_tools.rs index 31374d80b6..522bcfdcac 100644 --- a/codex-rs/app-server/src/dynamic_tools.rs +++ b/codex-rs/app-server/src/dynamic_tools.rs @@ -1,3 +1,4 @@ +use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallResponse; use codex_core::CodexThread; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; @@ -15,65 +16,23 @@ pub(crate) async fn on_call_response( conversation: Arc, ) { let response = receiver.await; - let value = match response { - Ok(Ok(value)) => value, + let (response, _error) = match response { + Ok(Ok(value)) => decode_response(value), Ok(Err(err)) => { error!("request failed with client error: {err:?}"); - let fallback = CoreDynamicToolResponse { - content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { - text: "dynamic tool request failed".to_string(), - }], - success: false, - }; - if let Err(err) = conversation - .submit(Op::DynamicToolResponse { - id: call_id.clone(), - response: fallback, - }) - .await - { - error!("failed to submit DynamicToolResponse: {err}"); - } - return; + fallback_response("dynamic tool request failed") } Err(err) => { error!("request failed: {err:?}"); - let fallback = CoreDynamicToolResponse { - content_items: vec![CoreDynamicToolCallOutputContentItem::InputText { - text: "dynamic tool request failed".to_string(), - }], - success: false, - }; - if let Err(err) = conversation - .submit(Op::DynamicToolResponse { - id: call_id.clone(), - response: fallback, - }) - .await - { - error!("failed to submit DynamicToolResponse: {err}"); - } - return; + fallback_response("dynamic tool request failed") } }; - let response = serde_json::from_value::(value).unwrap_or_else(|err| { - error!("failed to deserialize DynamicToolCallResponse: {err}"); - DynamicToolCallResponse { - content_items: vec![ - codex_app_server_protocol::DynamicToolCallOutputContentItem::InputText { - text: "dynamic tool response was invalid".to_string(), - }, - ], - success: false, - } - }); - let DynamicToolCallResponse { content_items, success, - } = response; - let response = CoreDynamicToolResponse { + } = response.clone(); + let core_response = CoreDynamicToolResponse { content_items: content_items .into_iter() .map(CoreDynamicToolCallOutputContentItem::from) @@ -82,11 +41,33 @@ pub(crate) async fn on_call_response( }; if let Err(err) = conversation .submit(Op::DynamicToolResponse { - id: call_id, - response, + id: call_id.clone(), + response: core_response, }) .await { error!("failed to submit DynamicToolResponse: {err}"); } } + +fn decode_response(value: serde_json::Value) -> (DynamicToolCallResponse, Option) { + match serde_json::from_value::(value) { + Ok(response) => (response, None), + Err(err) => { + error!("failed to deserialize DynamicToolCallResponse: {err}"); + fallback_response("dynamic tool response was invalid") + } + } +} + +fn fallback_response(message: &str) -> (DynamicToolCallResponse, Option) { + ( + DynamicToolCallResponse { + content_items: vec![DynamicToolCallOutputContentItem::InputText { + text: message.to_string(), + }], + success: false, + }, + Some(message.to_string()), + ) +} diff --git a/codex-rs/app-server/src/error_code.rs b/codex-rs/app-server/src/error_code.rs index ca93b2f2d3..924a7086ae 100644 --- a/codex-rs/app-server/src/error_code.rs +++ b/codex-rs/app-server/src/error_code.rs @@ -1,3 +1,5 @@ pub(crate) const INVALID_REQUEST_ERROR_CODE: i64 = -32600; +pub const INVALID_PARAMS_ERROR_CODE: i64 = -32602; pub(crate) const INTERNAL_ERROR_CODE: i64 = -32603; pub(crate) const OVERLOADED_ERROR_CODE: i64 = -32001; +pub const INPUT_TOO_LARGE_ERROR_CODE: &str = "input_too_large"; diff --git a/codex-rs/app-server/src/external_agent_config_api.rs b/codex-rs/app-server/src/external_agent_config_api.rs new file mode 100644 index 0000000000..7cf0d65d73 --- /dev/null +++ b/codex-rs/app-server/src/external_agent_config_api.rs @@ -0,0 +1,106 @@ +use crate::error_code::INTERNAL_ERROR_CODE; +use codex_app_server_protocol::ExternalAgentConfigDetectParams; +use codex_app_server_protocol::ExternalAgentConfigDetectResponse; +use codex_app_server_protocol::ExternalAgentConfigImportParams; +use codex_app_server_protocol::ExternalAgentConfigImportResponse; +use codex_app_server_protocol::ExternalAgentConfigMigrationItem; +use codex_app_server_protocol::ExternalAgentConfigMigrationItemType; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_core::external_agent_config::ExternalAgentConfigDetectOptions; +use codex_core::external_agent_config::ExternalAgentConfigMigrationItem as CoreMigrationItem; +use codex_core::external_agent_config::ExternalAgentConfigMigrationItemType as CoreMigrationItemType; +use codex_core::external_agent_config::ExternalAgentConfigService; +use std::io; +use std::path::PathBuf; + +#[derive(Clone)] +pub(crate) struct ExternalAgentConfigApi { + migration_service: ExternalAgentConfigService, +} + +impl ExternalAgentConfigApi { + pub(crate) fn new(codex_home: PathBuf) -> Self { + Self { + migration_service: ExternalAgentConfigService::new(codex_home), + } + } + + pub(crate) async fn detect( + &self, + params: ExternalAgentConfigDetectParams, + ) -> Result { + let items = self + .migration_service + .detect(ExternalAgentConfigDetectOptions { + include_home: params.include_home, + cwds: params.cwds, + }) + .map_err(map_io_error)?; + + Ok(ExternalAgentConfigDetectResponse { + items: items + .into_iter() + .map(|migration_item| ExternalAgentConfigMigrationItem { + item_type: match migration_item.item_type { + CoreMigrationItemType::Config => { + ExternalAgentConfigMigrationItemType::Config + } + CoreMigrationItemType::Skills => { + ExternalAgentConfigMigrationItemType::Skills + } + CoreMigrationItemType::AgentsMd => { + ExternalAgentConfigMigrationItemType::AgentsMd + } + CoreMigrationItemType::McpServerConfig => { + ExternalAgentConfigMigrationItemType::McpServerConfig + } + }, + description: migration_item.description, + cwd: migration_item.cwd, + }) + .collect(), + }) + } + + pub(crate) async fn import( + &self, + params: ExternalAgentConfigImportParams, + ) -> Result { + self.migration_service + .import( + params + .migration_items + .into_iter() + .map(|migration_item| CoreMigrationItem { + item_type: match migration_item.item_type { + ExternalAgentConfigMigrationItemType::Config => { + CoreMigrationItemType::Config + } + ExternalAgentConfigMigrationItemType::Skills => { + CoreMigrationItemType::Skills + } + ExternalAgentConfigMigrationItemType::AgentsMd => { + CoreMigrationItemType::AgentsMd + } + ExternalAgentConfigMigrationItemType::McpServerConfig => { + CoreMigrationItemType::McpServerConfig + } + }, + description: migration_item.description, + cwd: migration_item.cwd, + }) + .collect(), + ) + .map_err(map_io_error)?; + + Ok(ExternalAgentConfigImportResponse {}) + } +} + +fn map_io_error(err: io::Error) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: err.to_string(), + data: None, + } +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index b44661fd01..972f5af1e6 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::print_stdout, clippy::print_stderr)] +use codex_arg0::Arg0DispatchPaths; use codex_cloud_requirements::cloud_requirements_loader; use codex_core::AuthManager; use codex_core::config::Config; @@ -12,7 +13,6 @@ use std::collections::HashMap; use std::collections::HashSet; use std::io::ErrorKind; use std::io::Result as IoResult; -use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use std::sync::atomic::AtomicBool; @@ -57,6 +57,7 @@ mod codex_message_processor; mod config_api; mod dynamic_tools; mod error_code; +mod external_agent_config_api; mod filters; mod fuzzy_file_search; mod message_processor; @@ -66,6 +67,8 @@ mod thread_state; mod thread_status; mod transport; +pub use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE; +pub use crate::error_code::INVALID_PARAMS_ERROR_CODE; pub use crate::transport::AppServerTransport; const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT"; @@ -94,6 +97,7 @@ enum OutboundControlEvent { writer: mpsc::Sender, disconnect_sender: Option, initialized: Arc, + experimental_api_enabled: Arc, opted_out_notification_methods: Arc>>, }, /// Remove state for a closed/disconnected connection. @@ -291,13 +295,13 @@ fn log_format_from_env() -> LogFormat { } pub async fn run_main( - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, cli_config_overrides: CliConfigOverrides, loader_overrides: LoaderOverrides, default_analytics_enabled: bool, ) -> IoResult<()> { run_main_with_transport( - codex_linux_sandbox_exe, + arg0_paths, cli_config_overrides, loader_overrides, default_analytics_enabled, @@ -307,7 +311,7 @@ pub async fn run_main( } pub async fn run_main_with_transport( - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, cli_config_overrides: CliConfigOverrides, loader_overrides: LoaderOverrides, default_analytics_enabled: bool, @@ -503,6 +507,7 @@ pub async fn run_main_with_transport( writer, disconnect_sender, initialized, + experimental_api_enabled, opted_out_notification_methods, } => { outbound_connections.insert( @@ -510,6 +515,7 @@ pub async fn run_main_with_transport( OutboundConnectionState::new( writer, initialized, + experimental_api_enabled, opted_out_notification_methods, disconnect_sender, ), @@ -548,7 +554,7 @@ pub async fn run_main_with_transport( let loader_overrides = loader_overrides_for_config_api; let mut processor = MessageProcessor::new(MessageProcessorArgs { outgoing: outgoing_message_sender, - codex_linux_sandbox_exe, + arg0_paths, config: Arc::new(config), single_client_mode, cli_overrides, @@ -609,6 +615,8 @@ pub async fn run_main_with_transport( disconnect_sender, } => { let outbound_initialized = Arc::new(AtomicBool::new(false)); + let outbound_experimental_api_enabled = + Arc::new(AtomicBool::new(false)); let outbound_opted_out_notification_methods = Arc::new(RwLock::new(HashSet::new())); if outbound_control_tx @@ -617,6 +625,9 @@ pub async fn run_main_with_transport( writer, disconnect_sender, initialized: Arc::clone(&outbound_initialized), + experimental_api_enabled: Arc::clone( + &outbound_experimental_api_enabled, + ), opted_out_notification_methods: Arc::clone( &outbound_opted_out_notification_methods, ), @@ -630,6 +641,7 @@ pub async fn run_main_with_transport( connection_id, ConnectionState::new( outbound_initialized, + outbound_experimental_api_enabled, outbound_opted_out_notification_methods, ), ); @@ -679,6 +691,12 @@ pub async fn run_main_with_transport( "failed to update outbound opted-out notifications" ); } + connection_state + .outbound_experimental_api_enabled + .store( + connection_state.session.experimental_api_enabled, + std::sync::atomic::Ordering::Release, + ); if !was_initialized && connection_state.session.initialized { processor.send_initialize_notifications().await; } diff --git a/codex-rs/app-server/src/main.rs b/codex-rs/app-server/src/main.rs index 5c4e5eacc7..5c28413e88 100644 --- a/codex-rs/app-server/src/main.rs +++ b/codex-rs/app-server/src/main.rs @@ -1,6 +1,7 @@ use clap::Parser; use codex_app_server::AppServerTransport; use codex_app_server::run_main_with_transport; +use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_core::config_loader::LoaderOverrides; use codex_utils_cli::CliConfigOverrides; @@ -23,7 +24,7 @@ struct AppServerArgs { } fn main() -> anyhow::Result<()> { - arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { let args = AppServerArgs::parse(); let managed_config_path = managed_config_path_from_debug_env(); let loader_overrides = LoaderOverrides { @@ -33,7 +34,7 @@ fn main() -> anyhow::Result<()> { let transport = args.listen; run_main_with_transport( - codex_linux_sandbox_exe, + arg0_paths, CliConfigOverrides::default(), loader_overrides, false, diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index a4bb6a7389..79f845ad4b 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use std::sync::atomic::AtomicBool; @@ -9,6 +8,7 @@ use crate::codex_message_processor::CodexMessageProcessor; use crate::codex_message_processor::CodexMessageProcessorArgs; use crate::config_api::ConfigApi; use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use crate::external_agent_config_api::ExternalAgentConfigApi; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; @@ -23,6 +23,8 @@ use codex_app_server_protocol::ConfigReadParams; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::ExperimentalApi; +use codex_app_server_protocol::ExternalAgentConfigDetectParams; +use codex_app_server_protocol::ExternalAgentConfigImportParams; use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCErrorError; @@ -32,6 +34,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::experimental_required_message; +use codex_arg0::Arg0DispatchPaths; use codex_core::AuthManager; use codex_core::ThreadManager; use codex_core::auth::ExternalAuthRefreshContext; @@ -46,6 +49,7 @@ use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; use codex_core::default_client::set_default_client_residency_requirement; use codex_core::default_client::set_default_originator; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_feedback::CodexFeedback; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; @@ -84,7 +88,7 @@ impl ExternalAuthRefresher for ExternalAuthRefreshBridge { let (request_id, rx) = self .outgoing - .send_request_with_id(ServerRequestPayload::ChatgptAuthTokensRefresh(params)) + .send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params)) .await; let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await { @@ -126,6 +130,7 @@ pub(crate) struct MessageProcessor { outgoing: Arc, codex_message_processor: CodexMessageProcessor, config_api: ConfigApi, + external_agent_config_api: ExternalAgentConfigApi, config: Arc, config_warnings: Arc>, } @@ -133,13 +138,14 @@ pub(crate) struct MessageProcessor { #[derive(Clone, Debug, Default)] pub(crate) struct ConnectionSessionState { pub(crate) initialized: bool, - experimental_api_enabled: bool, + pub(crate) experimental_api_enabled: bool, pub(crate) opted_out_notification_methods: HashSet, + pub(crate) app_server_client_name: Option, } pub(crate) struct MessageProcessorArgs { pub(crate) outgoing: Arc, - pub(crate) codex_linux_sandbox_exe: Option, + pub(crate) arg0_paths: Arg0DispatchPaths, pub(crate) config: Arc, pub(crate) single_client_mode: bool, pub(crate) cli_overrides: Vec<(String, TomlValue)>, @@ -155,7 +161,7 @@ impl MessageProcessor { pub(crate) fn new(args: MessageProcessorArgs) -> Self { let MessageProcessorArgs { outgoing, - codex_linux_sandbox_exe, + arg0_paths, config, single_client_mode, cli_overrides, @@ -178,13 +184,18 @@ impl MessageProcessor { auth_manager.clone(), SessionSource::VSCode, config.model_catalog.clone(), + CollaborationModesConfig { + default_mode_request_user_input: config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + }, )); let cloud_requirements = Arc::new(RwLock::new(cloud_requirements)); let codex_message_processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { auth_manager, thread_manager, outgoing: outgoing.clone(), - codex_linux_sandbox_exe, + arg0_paths, config: Arc::clone(&config), cli_overrides: cli_overrides.clone(), cloud_requirements: cloud_requirements.clone(), @@ -197,11 +208,13 @@ impl MessageProcessor { loader_overrides, cloud_requirements, ); + let external_agent_config_api = ExternalAgentConfigApi::new(config.codex_home.clone()); Self { outgoing, codex_message_processor, config_api, + external_agent_config_api, config, config_warnings: Arc::new(config_warnings), } @@ -317,6 +330,7 @@ impl MessageProcessor { if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() { *suffix = Some(user_agent_suffix); } + session.app_server_client_name = Some(name.clone()); let user_agent = get_codex_user_agent(); let response = InitializeResponse { user_agent }; @@ -363,6 +377,26 @@ impl MessageProcessor { ) .await; } + ClientRequest::ExternalAgentConfigDetect { request_id, params } => { + self.handle_external_agent_config_detect( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::ExternalAgentConfigImport { request_id, params } => { + self.handle_external_agent_config_import( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } ClientRequest::ConfigValueWrite { request_id, params } => { self.handle_config_value_write( ConnectionRequestId { @@ -398,7 +432,7 @@ impl MessageProcessor { // inline the full `CodexMessageProcessor::process_request` future, which // can otherwise push worker-thread stack usage over the edge. self.codex_message_processor - .process_request(connection_id, other) + .process_request(connection_id, other, session.app_server_client_name.clone()) .boxed() .await; } @@ -492,4 +526,26 @@ impl MessageProcessor { Err(error) => self.outgoing.send_error(request_id, error).await, } } + + async fn handle_external_agent_config_detect( + &self, + request_id: ConnectionRequestId, + params: ExternalAgentConfigDetectParams, + ) { + match self.external_agent_config_api.detect(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_external_agent_config_import( + &self, + request_id: ConnectionRequestId, + params: ExternalAgentConfigImportParams, + ) { + match self.external_agent_config_api.import(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } } diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 1594c66229..6dbe77455f 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use codex_app_server_protocol::Model; +use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; use codex_core::models_manager::manager::RefreshStrategy; @@ -24,7 +25,14 @@ fn model_from_preset(preset: ModelPreset) -> Model { Model { id: preset.id.to_string(), model: preset.model.to_string(), - upgrade: preset.upgrade.map(|upgrade| upgrade.id), + upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()), + upgrade_info: preset.upgrade.as_ref().map(|upgrade| ModelUpgradeInfo { + model: upgrade.id.clone(), + upgrade_copy: upgrade.upgrade_copy.clone(), + model_link: upgrade.model_link.clone(), + migration_markdown: upgrade.migration_markdown.clone(), + }), + availability_nux: preset.availability_nux.map(Into::into), display_name: preset.display_name.to_string(), description: preset.description.to_string(), hidden: !preset.show_in_picker, diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index fe8f755883..44de783455 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::Result; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestPayload; +use codex_protocol::ThreadId; use serde::Serialize; use tokio::sync::Mutex; use tokio::sync::mpsc; @@ -48,23 +49,31 @@ pub(crate) enum OutgoingEnvelope { pub(crate) struct OutgoingMessageSender { next_server_request_id: AtomicI64, sender: mpsc::Sender, - request_id_to_callback: Mutex>>, + request_id_to_callback: Mutex>, } #[derive(Clone)] pub(crate) struct ThreadScopedOutgoingMessageSender { outgoing: Arc, connection_ids: Arc>, + thread_id: ThreadId, +} + +struct PendingCallbackEntry { + callback: oneshot::Sender, + thread_id: Option, } impl ThreadScopedOutgoingMessageSender { pub(crate) fn new( outgoing: Arc, connection_ids: Vec, + thread_id: ThreadId, ) -> Self { Self { outgoing, connection_ids: Arc::new(connection_ids), + thread_id, } } @@ -72,12 +81,12 @@ impl ThreadScopedOutgoingMessageSender { &self, payload: ServerRequestPayload, ) -> oneshot::Receiver { - if self.connection_ids.is_empty() { - let (_tx, rx) = oneshot::channel(); - return rx; - } self.outgoing - .send_request_to_connections(self.connection_ids.as_slice(), payload) + .send_request_to_thread_connections( + self.thread_id, + self.connection_ids.as_slice(), + payload, + ) .await } @@ -116,35 +125,52 @@ impl OutgoingMessageSender { } } - pub(crate) async fn send_request_to_connections( - &self, - connection_ids: &[ConnectionId], - request: ServerRequestPayload, - ) -> oneshot::Receiver { - let (_id, rx) = self - .send_request_with_id_to_connections(connection_ids, request) - .await; - rx - } - - pub(crate) async fn send_request_with_id( + pub(crate) async fn send_request( &self, request: ServerRequestPayload, ) -> (RequestId, oneshot::Receiver) { - self.send_request_with_id_to_connections(&[], request).await + self.send_request_with_id_to_connections(&[], request, None) + .await + } + + async fn send_request_to_thread_connections( + &self, + thread_id: ThreadId, + connection_ids: &[ConnectionId], + request: ServerRequestPayload, + ) -> oneshot::Receiver { + if connection_ids.is_empty() { + let (_tx, rx) = oneshot::channel(); + return rx; + } + let (_request_id, receiver) = self + .send_request_with_id_to_connections(connection_ids, request, Some(thread_id)) + .await; + receiver + } + + fn next_request_id(&self) -> RequestId { + RequestId::Integer(self.next_server_request_id.fetch_add(1, Ordering::Relaxed)) } async fn send_request_with_id_to_connections( &self, connection_ids: &[ConnectionId], request: ServerRequestPayload, + thread_id: Option, ) -> (RequestId, oneshot::Receiver) { - let id = RequestId::Integer(self.next_server_request_id.fetch_add(1, Ordering::Relaxed)); + let id = self.next_request_id(); let outgoing_message_id = id.clone(); let (tx_approve, rx_approve) = oneshot::channel(); { let mut request_id_to_callback = self.request_id_to_callback.lock().await; - request_id_to_callback.insert(id, tx_approve); + request_id_to_callback.insert( + id, + PendingCallbackEntry { + callback: tx_approve, + thread_id, + }, + ); } let outgoing_message = @@ -191,8 +217,8 @@ impl OutgoingMessageSender { }; match entry { - Some((id, sender)) => { - if let Err(err) = sender.send(Ok(result)) { + Some((id, entry)) => { + if let Err(err) = entry.callback.send(Ok(result)) { warn!("could not notify callback for {id:?} due to: {err:?}"); } } @@ -209,9 +235,9 @@ impl OutgoingMessageSender { }; match entry { - Some((id, sender)) => { + Some((id, entry)) => { warn!("client responded with error for {id:?}: {error:?}"); - if let Err(err) = sender.send(Err(error)) { + if let Err(err) = entry.callback.send(Err(error)) { warn!("could not notify callback for {id:?} due to: {err:?}"); } } @@ -229,6 +255,19 @@ impl OutgoingMessageSender { entry.is_some() } + pub(crate) async fn cancel_requests_for_thread(&self, thread_id: ThreadId) { + let mut request_id_to_callback = self.request_id_to_callback.lock().await; + let request_ids = request_id_to_callback + .iter() + .filter_map(|(request_id, entry)| { + (entry.thread_id == Some(thread_id)).then_some(request_id.clone()) + }) + .collect::>(); + for request_id in request_ids { + request_id_to_callback.remove(&request_id); + } + } + pub(crate) async fn send_response( &self, request_id: ConnectionRequestId, @@ -657,7 +696,7 @@ mod tests { let outgoing = OutgoingMessageSender::new(tx); let (request_id, wait_for_result) = outgoing - .send_request_with_id(ServerRequestPayload::ApplyPatchApproval( + .send_request(ServerRequestPayload::ApplyPatchApproval( ApplyPatchApprovalParams { conversation_id: ThreadId::new(), call_id: "call-id".to_string(), diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 19bb025de0..04e2a8240e 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -207,6 +207,50 @@ impl ThreadStateManager { }); } + pub(crate) async fn unsubscribe_connection_from_thread( + &mut self, + thread_id: ThreadId, + connection_id: ConnectionId, + ) -> bool { + let Some(thread_state) = self.thread_states.get(&thread_id) else { + return false; + }; + + if !self + .thread_ids_by_connection + .get(&connection_id) + .is_some_and(|thread_ids| thread_ids.contains(&thread_id)) + { + return false; + } + + if let Some(thread_ids) = self.thread_ids_by_connection.get_mut(&connection_id) { + thread_ids.remove(&thread_id); + if thread_ids.is_empty() { + self.thread_ids_by_connection.remove(&connection_id); + } + } + + self.subscription_state_by_id.retain(|_, state| { + !(state.thread_id == thread_id && state.connection_id == connection_id) + }); + + let mut thread_state = thread_state.lock().await; + thread_state.remove_connection(connection_id); + true + } + + pub(crate) async fn has_subscribers(&self, thread_id: ThreadId) -> bool { + let Some(thread_state) = self.thread_states.get(&thread_id) else { + return false; + }; + !thread_state + .lock() + .await + .subscribed_connection_ids() + .is_empty() + } + pub(crate) async fn set_listener( &mut self, subscription_id: Uuid, diff --git a/codex-rs/app-server/src/thread_status.rs b/codex-rs/app-server/src/thread_status.rs index 2eb1d8d036..f3a7c1fd83 100644 --- a/codex-rs/app-server/src/thread_status.rs +++ b/codex-rs/app-server/src/thread_status.rs @@ -300,8 +300,16 @@ impl ThreadWatchState { } fn remove_thread(&mut self, thread_id: &str) -> Option { + let previous_status = self.status_for(thread_id); self.runtime_by_thread_id.remove(thread_id); - None + if previous_status.is_some() && previous_status != Some(ThreadStatus::NotLoaded) { + Some(ThreadStatusChangedNotification { + thread_id: thread_id.to_string(), + status: ThreadStatus::NotLoaded, + }) + } else { + None + } } fn update_runtime( @@ -673,6 +681,15 @@ mod tests { }, }, ); + + manager.remove_thread(INTERACTIVE_THREAD_ID).await; + assert_eq!( + recv_status_changed_notification(&mut outgoing_rx).await, + ThreadStatusChangedNotification { + thread_id: INTERACTIVE_THREAD_ID.to_string(), + status: ThreadStatus::NotLoaded, + }, + ); } async fn wait_for_status( diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 027522153e..2bae5b44e9 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -6,6 +6,7 @@ use crate::outgoing_message::OutgoingError; use crate::outgoing_message::OutgoingMessage; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::ServerRequest; use futures::SinkExt; use futures::StreamExt; use owo_colors::OwoColorize; @@ -144,6 +145,7 @@ pub(crate) enum TransportEvent { pub(crate) struct ConnectionState { pub(crate) outbound_initialized: Arc, + pub(crate) outbound_experimental_api_enabled: Arc, pub(crate) outbound_opted_out_notification_methods: Arc>>, pub(crate) session: ConnectionSessionState, } @@ -151,10 +153,12 @@ pub(crate) struct ConnectionState { impl ConnectionState { pub(crate) fn new( outbound_initialized: Arc, + outbound_experimental_api_enabled: Arc, outbound_opted_out_notification_methods: Arc>>, ) -> Self { Self { outbound_initialized, + outbound_experimental_api_enabled, outbound_opted_out_notification_methods, session: ConnectionSessionState::default(), } @@ -163,6 +167,7 @@ impl ConnectionState { pub(crate) struct OutboundConnectionState { pub(crate) initialized: Arc, + pub(crate) experimental_api_enabled: Arc, pub(crate) opted_out_notification_methods: Arc>>, pub(crate) writer: mpsc::Sender, disconnect_sender: Option, @@ -172,11 +177,13 @@ impl OutboundConnectionState { pub(crate) fn new( writer: mpsc::Sender, initialized: Arc, + experimental_api_enabled: Arc, opted_out_notification_methods: Arc>>, disconnect_sender: Option, ) -> Self { Self { initialized, + experimental_api_enabled, opted_out_notification_methods, writer, disconnect_sender, @@ -577,6 +584,7 @@ async fn send_message_to_connection( warn!("dropping message for disconnected connection: {connection_id:?}"); return false; }; + let message = filter_outgoing_message_for_connection(connection_state, message); if should_skip_notification_for_connection(connection_state, &message) { return false; } @@ -602,6 +610,30 @@ async fn send_message_to_connection( } } +fn filter_outgoing_message_for_connection( + connection_state: &OutboundConnectionState, + message: OutgoingMessage, +) -> OutgoingMessage { + let experimental_api_enabled = connection_state + .experimental_api_enabled + .load(Ordering::Acquire); + match message { + OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval { + request_id, + mut params, + }) => { + if !experimental_api_enabled { + params.strip_experimental_fields(); + } + OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval { + request_id, + params, + }) + } + _ => message, + } +} + pub(crate) async fn route_outgoing_envelope( connections: &mut HashMap, envelope: OutgoingEnvelope, @@ -639,11 +671,17 @@ pub(crate) async fn route_outgoing_envelope( mod tests { use super::*; use crate::error_code::OVERLOADED_ERROR_CODE; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; + use std::path::PathBuf; use tokio::time::Duration; use tokio::time::timeout; + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(path).expect("absolute path") + } + #[test] fn app_server_transport_parses_stdio_listen_url() { let transport = AppServerTransport::from_listen_url(AppServerTransport::DEFAULT_LISTEN_URL) @@ -880,6 +918,7 @@ mod tests { OutboundConnectionState::new( writer_tx, initialized, + Arc::new(AtomicBool::new(true)), opted_out_notification_methods, None, ), @@ -905,6 +944,141 @@ mod tests { ); } + #[tokio::test] + async fn command_execution_request_approval_strips_experimental_fields_without_capability() { + let connection_id = ConnectionId(8); + let (writer_tx, mut writer_rx) = mpsc::channel(1); + + let mut connections = HashMap::new(); + connections.insert( + connection_id, + OutboundConnectionState::new( + writer_tx, + Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(false)), + Arc::new(RwLock::new(HashSet::new())), + None, + ), + ); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::ToConnection { + connection_id, + message: OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval { + request_id: codex_app_server_protocol::RequestId::Integer(1), + params: codex_app_server_protocol::CommandExecutionRequestApprovalParams { + thread_id: "thr_123".to_string(), + turn_id: "turn_123".to_string(), + item_id: "call_123".to_string(), + approval_id: None, + reason: Some("Need extra read access".to_string()), + network_approval_context: None, + command: Some("cat file".to_string()), + cwd: Some(PathBuf::from("/tmp")), + command_actions: None, + additional_permissions: Some( + codex_app_server_protocol::AdditionalPermissionProfile { + network: None, + file_system: Some( + codex_app_server_protocol::AdditionalFileSystemPermissions { + read: Some(vec![absolute_path("/tmp/allowed")]), + write: None, + }, + ), + macos: None, + }, + ), + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + available_decisions: None, + }, + }), + }, + ) + .await; + + let message = writer_rx + .recv() + .await + .expect("request should be delivered to the connection"); + let json = serde_json::to_value(message).expect("request should serialize"); + assert_eq!(json["params"].get("additionalPermissions"), None); + } + + #[tokio::test] + async fn command_execution_request_approval_keeps_experimental_fields_with_capability() { + let connection_id = ConnectionId(9); + let (writer_tx, mut writer_rx) = mpsc::channel(1); + + let mut connections = HashMap::new(); + connections.insert( + connection_id, + OutboundConnectionState::new( + writer_tx, + Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(true)), + Arc::new(RwLock::new(HashSet::new())), + None, + ), + ); + + route_outgoing_envelope( + &mut connections, + OutgoingEnvelope::ToConnection { + connection_id, + message: OutgoingMessage::Request(ServerRequest::CommandExecutionRequestApproval { + request_id: codex_app_server_protocol::RequestId::Integer(1), + params: codex_app_server_protocol::CommandExecutionRequestApprovalParams { + thread_id: "thr_123".to_string(), + turn_id: "turn_123".to_string(), + item_id: "call_123".to_string(), + approval_id: None, + reason: Some("Need extra read access".to_string()), + network_approval_context: None, + command: Some("cat file".to_string()), + cwd: Some(PathBuf::from("/tmp")), + command_actions: None, + additional_permissions: Some( + codex_app_server_protocol::AdditionalPermissionProfile { + network: None, + file_system: Some( + codex_app_server_protocol::AdditionalFileSystemPermissions { + read: Some(vec![absolute_path("/tmp/allowed")]), + write: None, + }, + ), + macos: None, + }, + ), + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + available_decisions: None, + }, + }), + }, + ) + .await; + + let message = writer_rx + .recv() + .await + .expect("request should be delivered to the connection"); + let json = serde_json::to_value(message).expect("request should serialize"); + let allowed_path = absolute_path("/tmp/allowed").to_string_lossy().into_owned(); + assert_eq!( + json["params"]["additionalPermissions"], + json!({ + "network": null, + "fileSystem": { + "read": [allowed_path], + "write": null, + }, + "macos": null, + }) + ); + } + #[tokio::test] async fn broadcast_does_not_block_on_slow_connection() { let fast_connection_id = ConnectionId(1); @@ -921,6 +1095,7 @@ mod tests { OutboundConnectionState::new( fast_writer_tx, Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(true)), Arc::new(RwLock::new(HashSet::new())), Some(fast_disconnect_token.clone()), ), @@ -930,6 +1105,7 @@ mod tests { OutboundConnectionState::new( slow_writer_tx.clone(), Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(true)), Arc::new(RwLock::new(HashSet::new())), Some(slow_disconnect_token.clone()), ), @@ -1006,6 +1182,7 @@ mod tests { OutboundConnectionState::new( writer_tx, Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(true)), Arc::new(RwLock::new(HashSet::new())), None, ), diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index bb35c2c0ab..cb0c98806f 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -57,11 +57,16 @@ use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadLoadedListParams; use codex_app_server_protocol::ThreadReadParams; +use codex_app_server_protocol::ThreadRealtimeAppendAudioParams; +use codex_app_server_protocol::ThreadRealtimeAppendTextParams; +use codex_app_server_protocol::ThreadRealtimeStartParams; +use codex_app_server_protocol::ThreadRealtimeStopParams; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadRollbackParams; use codex_app_server_protocol::ThreadSetNameParams; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadUnarchiveParams; +use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnStartParams; @@ -429,6 +434,15 @@ impl McpProcess { self.send_request("thread/name/set", params).await } + /// Send a `thread/unsubscribe` JSON-RPC request. + pub async fn send_thread_unsubscribe_request( + &mut self, + params: ThreadUnsubscribeParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/unsubscribe", params).await + } + /// Send a `thread/unarchive` JSON-RPC request. pub async fn send_thread_unarchive_request( &mut self, @@ -584,6 +598,44 @@ impl McpProcess { self.send_request("turn/interrupt", params).await } + /// Send a `thread/realtime/start` JSON-RPC request (v2). + pub async fn send_thread_realtime_start_request( + &mut self, + params: ThreadRealtimeStartParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/realtime/start", params).await + } + + /// Send a `thread/realtime/appendAudio` JSON-RPC request (v2). + pub async fn send_thread_realtime_append_audio_request( + &mut self, + params: ThreadRealtimeAppendAudioParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/realtime/appendAudio", params) + .await + } + + /// Send a `thread/realtime/appendText` JSON-RPC request (v2). + pub async fn send_thread_realtime_append_text_request( + &mut self, + params: ThreadRealtimeAppendTextParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/realtime/appendText", params) + .await + } + + /// Send a `thread/realtime/stop` JSON-RPC request (v2). + pub async fn send_thread_realtime_stop_request( + &mut self, + params: ThreadRealtimeStopParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("thread/realtime/stop", params).await + } + /// Deterministically clean up an intentionally in-flight turn. /// /// Some tests assert behavior while a turn is still running. Returning from those tests diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 218a1d0e40..0de8fda5f2 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -1,6 +1,7 @@ use chrono::DateTime; use chrono::Utc; use codex_core::test_support::all_model_presets; +use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; @@ -30,8 +31,10 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/app-server/tests/suite/output_schema.rs b/codex-rs/app-server/tests/suite/output_schema.rs index 3d83bec7dd..db006e3284 100644 --- a/codex-rs/app-server/tests/suite/output_schema.rs +++ b/codex-rs/app-server/tests/suite/output_schema.rs @@ -1,8 +1,11 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::to_response; +use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; +use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::AddConversationListenerParams; use codex_app_server_protocol::InputItem; +use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; @@ -13,6 +16,7 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; @@ -124,6 +128,85 @@ async fn send_user_turn_accepts_output_schema_v1() -> Result<()> { Ok(()) } +#[tokio::test] +async fn send_user_turn_rejects_oversized_input_v1() -> Result<()> { + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let _response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let new_conv_id = mcp + .send_new_conversation_request(NewConversationParams { + ..Default::default() + }) + .await?; + let new_conv_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)), + ) + .await??; + let NewConversationResponse { + conversation_id, .. + } = to_response::(new_conv_resp)?; + + let listener_id = mcp + .send_add_conversation_listener_request(AddConversationListenerParams { + conversation_id, + experimental_raw_events: false, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(listener_id)), + ) + .await??; + + let oversized_input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); + let send_turn_id = mcp + .send_send_user_turn_request(SendUserTurnParams { + conversation_id, + items: vec![InputItem::Text { + text: oversized_input.clone(), + text_elements: Vec::new(), + }], + cwd: codex_home.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + model: "mock-model".to_string(), + effort: Some(ReasoningEffort::Low), + summary: ReasoningSummary::Auto, + output_schema: None, + }) + .await?; + + let err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(send_turn_id)), + ) + .await??; + + assert_eq!(err.error.code, INVALID_PARAMS_ERROR_CODE); + assert_eq!( + err.error.message, + format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.") + ); + let data = err.error.data.expect("expected structured error data"); + assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE); + assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS); + assert_eq!(data["actual_chars"], oversized_input.chars().count()); + + Ok(()) +} + #[tokio::test] async fn send_user_turn_output_schema_is_per_turn_v1() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/app-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs index 0bf6e98cfb..e755a11b77 100644 --- a/codex-rs/app-server/tests/suite/send_message.rs +++ b/codex-rs/app-server/tests/suite/send_message.rs @@ -3,9 +3,12 @@ use app_test_support::McpProcess; use app_test_support::create_fake_rollout; use app_test_support::rollout_path; use app_test_support::to_response; +use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; +use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::AddConversationListenerParams; use codex_app_server_protocol::AddConversationSubscriptionResponse; use codex_app_server_protocol::InputItem; +use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::NewConversationParams; @@ -27,6 +30,7 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnContextItem; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use core_test_support::responses; use pretty_assertions::assert_eq; use std::io::Write; @@ -210,17 +214,13 @@ async fn test_send_message_raw_notifications_opt_in() -> Result<()> { }) .await?; - let permissions = read_raw_response_item(&mut mcp, conversation_id).await; - assert_permissions_message(&permissions); - let developer = read_raw_response_item(&mut mcp, conversation_id).await; + assert_permissions_message(&developer); assert_developer_message(&developer, "Use the test harness tools."); - let instructions = read_raw_response_item(&mut mcp, conversation_id).await; - assert_instructions_message(&instructions); - - let environment = read_raw_response_item(&mut mcp, conversation_id).await; - assert_environment_message(&environment); + let contextual_user = read_raw_response_item(&mut mcp, conversation_id).await; + assert_instructions_message(&contextual_user); + assert_environment_message(&contextual_user); let response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -272,6 +272,66 @@ async fn test_send_message_session_not_found() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_send_message_rejects_oversized_input() -> Result<()> { + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let _response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let new_conv_id = mcp + .send_new_conversation_request(NewConversationParams { + ..Default::default() + }) + .await?; + let new_conv_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)), + ) + .await??; + let NewConversationResponse { + conversation_id, .. + } = to_response::<_>(new_conv_resp)?; + + let oversized_input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); + let req_id = mcp + .send_send_user_message_request(SendUserMessageParams { + conversation_id, + items: vec![InputItem::Text { + text: oversized_input.clone(), + text_elements: Vec::new(), + }], + }) + .await?; + + let err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(req_id)), + ) + .await??; + + assert_eq!(err.error.code, INVALID_PARAMS_ERROR_CODE); + assert_eq!( + err.error.message, + format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.") + ); + let data = err.error.data.expect("expected structured error data"); + assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE); + assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS); + assert_eq!(data["actual_chars"], oversized_input.chars().count()); + + Ok(()) +} + #[tokio::test] async fn resume_with_model_mismatch_appends_model_switch_once() -> Result<()> { let server = responses::start_mock_server().await; @@ -481,9 +541,8 @@ fn assert_permissions_message(item: &ResponseItem) { false, ) .into_text(); - assert_eq!( - texts, - vec![expected.as_str()], + assert!( + texts.iter().any(|text| *text == expected), "expected permissions developer message, got {texts:?}" ); } @@ -496,9 +555,8 @@ fn assert_developer_message(item: &ResponseItem, expected_text: &str) { ResponseItem::Message { role, content, .. } => { assert_eq!(role, "developer"); let texts = content_texts(content); - assert_eq!( - texts, - vec![expected_text], + assert!( + texts.contains(&expected_text), "expected developer instructions message, got {texts:?}" ); } @@ -562,6 +620,8 @@ fn append_rollout_turn_context(path: &Path, timestamp: &str, model: &str) -> std item: RolloutItem::TurnContext(TurnContextItem { turn_id: None, cwd: PathBuf::from("/"), + current_date: None, + timezone: None, approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::DangerFullAccess, network: None, diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index a9bc14e932..6a3243ad56 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -994,6 +994,41 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu let first_update = read_app_list_updated_notification(&mut mcp).await?; assert_eq!( first_update.data, + vec![ + AppInfo { + id: "beta".to_string(), + name: "Beta App".to_string(), + description: Some("Beta v1".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), + is_accessible: true, + is_enabled: true, + }, + AppInfo { + id: "alpha".to_string(), + name: "Alpha".to_string(), + description: Some("Alpha v1".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), + is_accessible: false, + is_enabled: true, + }, + ] + ); + + let second_update = read_app_list_updated_notification(&mut mcp).await?; + assert_eq!( + second_update.data, vec![ AppInfo { id: "alpha".to_string(), @@ -1040,8 +1075,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu is_accessible: false, is_enabled: true, }]; - let second_update = read_app_list_updated_notification(&mut mcp).await?; - assert_eq!(second_update.data, expected_final); + let third_update = read_app_list_updated_notification(&mut mcp).await?; + assert_eq!(third_update.data, expected_final); let refetch_response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index 4435542909..7c36827e6d 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -13,11 +13,10 @@ use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; +use codex_app_server_protocol::CollaborationModeMask; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_core::test_support::builtin_collaboration_mode_presets; -use codex_protocol::config_types::CollaborationModeMask; -use codex_protocol::config_types::ModeKind; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -33,7 +32,7 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_list_collaboration_modes_request(CollaborationModeListParams {}) + .send_list_collaboration_modes_request(CollaborationModeListParams::default()) .await?; let response: JSONRPCResponse = timeout( @@ -45,28 +44,15 @@ async fn list_collaboration_modes_returns_presets() -> Result<()> { let CollaborationModeListResponse { data: items } = to_response::(response)?; - let expected = vec![plan_preset(), default_preset()]; + let expected: Vec = builtin_collaboration_mode_presets() + .into_iter() + .map(|preset| CollaborationModeMask { + name: preset.name, + mode: preset.mode, + model: preset.model, + reasoning_effort: preset.reasoning_effort, + }) + .collect(); assert_eq!(expected, items); Ok(()) } - -/// Builds the plan preset that the list response is expected to return. -/// -/// If the defaults change in the app server, this helper should be updated alongside the -/// contract, or the test will fail in ways that imply a regression in the API. -fn plan_preset() -> CollaborationModeMask { - let presets = builtin_collaboration_mode_presets(); - presets - .into_iter() - .find(|p| p.mode == Some(ModeKind::Plan)) - .unwrap() -} - -/// Builds the default preset that the list response is expected to return. -fn default_preset() -> CollaborationModeMask { - let presets = builtin_collaboration_mode_presets(); - presets - .into_iter() - .find(|p| p.mode == Some(ModeKind::Default)) - .unwrap() -} diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index f4cd85a61d..52a597f66b 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -7,10 +7,15 @@ use app_test_support::to_response; use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallParams; use codex_app_server_protocol::DynamicToolCallResponse; +use codex_app_server_protocol::DynamicToolCallStatus; use codex_app_server_protocol::DynamicToolSpec; +use codex_app_server_protocol::ItemCompletedNotification; +use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -163,11 +168,12 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res ) .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + let thread_id = thread.id.clone(); // Start a turn so the tool call is emitted. let turn_req = mcp .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -181,6 +187,30 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res ) .await??; let TurnStartResponse { turn } = to_response::(turn_resp)?; + let turn_id = turn.id.clone(); + + let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?; + assert_eq!(started.thread_id, thread_id); + assert_eq!(started.turn_id, turn_id.clone()); + let ThreadItem::DynamicToolCall { + id, + tool, + arguments, + status, + content_items, + success, + duration_ms, + } = started.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(id, call_id); + assert_eq!(tool, tool_name); + assert_eq!(arguments, tool_args); + assert_eq!(status, DynamicToolCallStatus::InProgress); + assert_eq!(content_items, None); + assert_eq!(success, None); + assert_eq!(duration_ms, None); // Read the tool call request from the app server. let request = timeout( @@ -194,8 +224,8 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res }; let expected = DynamicToolCallParams { - thread_id: thread.id, - turn_id: turn.id, + thread_id: thread_id.clone(), + turn_id: turn_id.clone(), call_id: call_id.to_string(), tool: tool_name.to_string(), arguments: tool_args.clone(), @@ -212,6 +242,34 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res mcp.send_response(request_id, serde_json::to_value(response)?) .await?; + let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; + assert_eq!(completed.thread_id, thread_id); + assert_eq!(completed.turn_id, turn_id); + let ThreadItem::DynamicToolCall { + id, + tool, + arguments, + status, + content_items, + success, + duration_ms, + } = completed.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(id, call_id); + assert_eq!(tool, tool_name); + assert_eq!(arguments, tool_args); + assert_eq!(status, DynamicToolCallStatus::Completed); + assert_eq!( + content_items, + Some(vec![DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }]) + ); + assert_eq!(success, Some(true)); + assert!(duration_ms.is_some()); + timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), @@ -282,10 +340,11 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( ) .await??; let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + let thread_id = thread.id.clone(); let turn_req = mcp .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), + thread_id: thread_id.clone(), input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -299,6 +358,11 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( ) .await??; let TurnStartResponse { turn } = to_response::(turn_resp)?; + let turn_id = turn.id.clone(); + + let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?; + assert_eq!(started.thread_id, thread_id.clone()); + assert_eq!(started.turn_id, turn_id.clone()); let request = timeout( DEFAULT_READ_TIMEOUT, @@ -311,8 +375,8 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( }; let expected = DynamicToolCallParams { - thread_id: thread.id, - turn_id: turn.id, + thread_id, + turn_id: turn_id.clone(), call_id: call_id.to_string(), tool: tool_name.to_string(), arguments: tool_args, @@ -346,6 +410,32 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( mcp.send_response(request_id, serde_json::to_value(response)?) .await?; + let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?; + assert_eq!(completed.thread_id, expected.thread_id.clone()); + assert_eq!(completed.turn_id, turn_id); + let ThreadItem::DynamicToolCall { + status, + content_items: completed_content_items, + success, + .. + } = completed.item + else { + panic!("expected dynamic tool call item"); + }; + assert_eq!(status, DynamicToolCallStatus::Completed); + assert_eq!( + completed_content_items, + Some(vec![ + DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }, + DynamicToolCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,AAA".to_string(), + }, + ]) + ); + assert_eq!(success, Some(true)); + timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("turn/completed"), @@ -432,6 +522,46 @@ fn function_call_output_raw_output(body: &Value, call_id: &str) -> Option .cloned() } +async fn wait_for_dynamic_tool_started( + mcp: &mut McpProcess, + call_id: &str, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/started"), + ) + .await??; + let Some(params) = notification.params else { + continue; + }; + let started: ItemStartedNotification = serde_json::from_value(params)?; + if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + return Ok(started); + } + } +} + +async fn wait_for_dynamic_tool_completed( + mcp: &mut McpProcess, + call_id: &str, +) -> Result { + loop { + let notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("item/completed"), + ) + .await??; + let Some(params) = notification.params else { + continue; + }; + let completed: ItemCompletedNotification = serde_json::from_value(params)?; + if matches!(&completed.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) { + return Ok(completed); + } + } +} + fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 798d52abfd..9af3aa4c7a 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -10,6 +10,7 @@ use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadRealtimeStartParams; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use pretty_assertions::assert_eq; @@ -50,6 +51,40 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu Ok(()) } +#[tokio::test] +async fn realtime_conversation_start_requires_experimental_api_capability() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + + let init = mcp + .initialize_with_capabilities( + default_client_info(), + Some(InitializeCapabilities { + experimental_api: false, + opt_out_notification_methods: None, + }), + ) + .await?; + let JSONRPCMessage::Response(_) = init else { + anyhow::bail!("expected initialize response, got {init:?}"); + }; + + let request_id = mcp + .send_thread_realtime_start_request(ThreadRealtimeStartParams { + thread_id: "thr_123".to_string(), + prompt: "hello".to_string(), + session_id: None, + }) + .await?; + let error = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_experimental_capability_error(error, "thread/realtime/start"); + Ok(()) +} + #[tokio::test] async fn thread_start_mock_field_requires_experimental_api_capability() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 2edc83a7f4..de437c5bf5 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -1,16 +1,24 @@ use anyhow::Result; use app_test_support::McpProcess; +use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use core_test_support::fs_wait; use pretty_assertions::assert_eq; +use serde_json::Value; use std::path::Path; +use std::time::Duration; use tempfile::TempDir; use tokio::time::timeout; @@ -178,11 +186,103 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu Ok(()) } +#[tokio::test] +async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + let notify_script = codex_home.path().join("notify.py"); + std::fs::write( + ¬ify_script, + r#"from pathlib import Path +import sys + +payload_path = Path(__file__).with_name("notify.json") +tmp_path = payload_path.with_suffix(".json.tmp") +tmp_path.write_text(sys.argv[-1], encoding="utf-8") +tmp_path.replace(payload_path) +"#, + )?; + let notify_file = codex_home.path().join("notify.json"); + let notify_script = notify_script + .to_str() + .expect("notify script path should be valid UTF-8"); + create_config_toml_with_extra( + codex_home.path(), + &server.uri(), + "never", + &format!( + "notify = [\"python3\", {}]", + toml_basic_string(notify_script) + ), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.initialize_with_client_info(ClientInfo { + name: "xcode".to_string(), + title: Some("Xcode".to_string()), + version: "1.0.0".to_string(), + }), + ) + .await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _: TurnStartResponse = to_response(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + fs_wait::wait_for_path_exists(¬ify_file, Duration::from_secs(5)).await?; + let payload_raw = tokio::fs::read_to_string(¬ify_file).await?; + let payload: Value = serde_json::from_str(&payload_raw)?; + assert_eq!(payload["client"], "xcode"); + + Ok(()) +} + // Helper to create a config.toml pointing at the mock model server. fn create_config_toml( codex_home: &Path, server_uri: &str, approval_policy: &str, +) -> std::io::Result<()> { + create_config_toml_with_extra(codex_home, server_uri, approval_policy, "") +} + +fn create_config_toml_with_extra( + codex_home: &Path, + server_uri: &str, + approval_policy: &str, + extra: &str, ) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( @@ -195,6 +295,8 @@ sandbox_mode = "read-only" model_provider = "mock_provider" +{extra} + [model_providers.mock_provider] name = "Mock provider for test" base_url = "{server_uri}/v1" @@ -205,3 +307,7 @@ stream_max_retries = 0 ), ) } + +fn toml_basic_string(value: &str) -> String { + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 67e7ae3ea6..cb3acb633d 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -15,10 +15,10 @@ mod model_list; mod output_schema; mod plan_item; mod rate_limits; +mod realtime_conversation; mod request_user_input; mod review; mod safety_check_downgrade; -mod skill_approval; mod skills_list; mod thread_archive; mod thread_fork; @@ -30,6 +30,7 @@ mod thread_rollback; mod thread_start; mod thread_status; mod thread_unarchive; +mod thread_unsubscribe; mod turn_interrupt; mod turn_start; mod turn_start_zsh_fork; diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index 7597212490..eba8905a77 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -1,7 +1,6 @@ use std::time::Duration; use anyhow::Result; -use anyhow::anyhow; use app_test_support::McpProcess; use app_test_support::to_response; use app_test_support::write_models_cache; @@ -10,6 +9,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::Model; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_app_server_protocol::RequestId; use codex_protocol::openai_models::ModelPreset; @@ -25,6 +25,13 @@ fn model_from_preset(preset: &ModelPreset) -> Model { id: preset.id.clone(), model: preset.model.clone(), upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()), + upgrade_info: preset.upgrade.as_ref().map(|upgrade| ModelUpgradeInfo { + model: upgrade.id.clone(), + upgrade_copy: upgrade.upgrade_copy.clone(), + model_link: upgrade.model_link.clone(), + migration_markdown: upgrade.migration_markdown.clone(), + }), + availability_nux: preset.availability_nux.clone().map(Into::into), display_name: preset.display_name.clone(), description: preset.description.clone(), hidden: !preset.show_in_picker, @@ -136,100 +143,45 @@ async fn list_models_pagination_works() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - let first_request = mcp - .send_list_models_request(ModelListParams { - limit: Some(1), - cursor: None, - include_hidden: None, - }) - .await?; - - let first_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(first_request)), - ) - .await??; - - let ModelListResponse { - data: first_items, - next_cursor: first_cursor, - } = to_response::(first_response)?; - let expected_models = expected_visible_models(); + let mut cursor = None; + let mut items = Vec::new(); - assert_eq!(first_items.len(), 1); - assert_eq!(first_items[0].id, expected_models[0].id); - let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?; + for _ in 0..expected_models.len() { + let request_id = mcp + .send_list_models_request(ModelListParams { + limit: Some(1), + cursor: cursor.clone(), + include_hidden: None, + }) + .await?; - let second_request = mcp - .send_list_models_request(ModelListParams { - limit: Some(1), - cursor: Some(next_cursor.clone()), - include_hidden: None, - }) - .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; - let second_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(second_request)), - ) - .await??; + let ModelListResponse { + data: page_items, + next_cursor, + } = to_response::(response)?; - let ModelListResponse { - data: second_items, - next_cursor: second_cursor, - } = to_response::(second_response)?; + assert_eq!(page_items.len(), 1); + items.extend(page_items); - assert_eq!(second_items.len(), 1); - assert_eq!(second_items[0].id, expected_models[1].id); - let third_cursor = second_cursor.ok_or_else(|| anyhow!("cursor for third page"))?; + if let Some(next_cursor) = next_cursor { + cursor = Some(next_cursor); + } else { + assert_eq!(items, expected_models); + return Ok(()); + } + } - let third_request = mcp - .send_list_models_request(ModelListParams { - limit: Some(1), - cursor: Some(third_cursor.clone()), - include_hidden: None, - }) - .await?; - - let third_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(third_request)), - ) - .await??; - - let ModelListResponse { - data: third_items, - next_cursor: third_cursor, - } = to_response::(third_response)?; - - assert_eq!(third_items.len(), 1); - assert_eq!(third_items[0].id, expected_models[2].id); - let fourth_cursor = third_cursor.ok_or_else(|| anyhow!("cursor for fourth page"))?; - - let fourth_request = mcp - .send_list_models_request(ModelListParams { - limit: Some(1), - cursor: Some(fourth_cursor.clone()), - include_hidden: None, - }) - .await?; - - let fourth_response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(fourth_request)), - ) - .await??; - - let ModelListResponse { - data: fourth_items, - next_cursor: fourth_cursor, - } = to_response::(fourth_response)?; - - assert_eq!(fourth_items.len(), 1); - assert_eq!(fourth_items[0].id, expected_models[3].id); - assert!(fourth_cursor.is_none()); - Ok(()) + panic!( + "model pagination did not terminate after {} pages", + expected_models.len() + ); } #[tokio::test] diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs new file mode 100644 index 0000000000..f2664848ca --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -0,0 +1,392 @@ +use anyhow::Context; +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::create_mock_responses_server_sequence_unchecked; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCError; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadRealtimeAppendAudioParams; +use codex_app_server_protocol::ThreadRealtimeAppendAudioResponse; +use codex_app_server_protocol::ThreadRealtimeAppendTextParams; +use codex_app_server_protocol::ThreadRealtimeAppendTextResponse; +use codex_app_server_protocol::ThreadRealtimeAudioChunk; +use codex_app_server_protocol::ThreadRealtimeClosedNotification; +use codex_app_server_protocol::ThreadRealtimeErrorNotification; +use codex_app_server_protocol::ThreadRealtimeItemAddedNotification; +use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification; +use codex_app_server_protocol::ThreadRealtimeStartParams; +use codex_app_server_protocol::ThreadRealtimeStartResponse; +use codex_app_server_protocol::ThreadRealtimeStartedNotification; +use codex_app_server_protocol::ThreadRealtimeStopParams; +use codex_app_server_protocol::ThreadRealtimeStopResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_core::features::FEATURES; +use codex_core::features::Feature; +use core_test_support::responses::start_websocket_server; +use core_test_support::skip_if_no_network; +use pretty_assertions::assert_eq; +use serde::de::DeserializeOwned; +use serde_json::json; +use std::path::Path; +use std::time::Duration; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); + +#[tokio::test] +async fn realtime_conversation_streams_v2_notifications() -> Result<()> { + skip_if_no_network!(Ok(())); + + let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let realtime_server = start_websocket_server(vec![vec![ + vec![json!({ + "type": "session.created", + "session": { "id": "sess_backend" } + })], + vec![json!({ + "type": "session.updated", + "session": { "backend_prompt": "backend prompt" } + })], + vec![ + json!({ + "type": "response.output_audio.delta", + "delta": "AQID", + "sample_rate": 24_000, + "num_channels": 1, + "samples_per_channel": 512 + }), + json!({ + "type": "conversation.item.added", + "item": { + "type": "message", + "role": "assistant", + "content": [{ "type": "text", "text": "hi" }] + } + }), + json!({ + "type": "error", + "message": "upstream boom" + }), + ], + ]]) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &responses_server.uri(), + realtime_server.uri(), + true, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + mcp.initialize().await?; + + let thread_start_request_id = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_start_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_request_id)), + ) + .await??; + let thread_start: ThreadStartResponse = to_response(thread_start_response)?; + + let start_request_id = mcp + .send_thread_realtime_start_request(ThreadRealtimeStartParams { + thread_id: thread_start.thread.id.clone(), + prompt: "backend prompt".to_string(), + session_id: None, + }) + .await?; + let start_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(start_request_id)), + ) + .await??; + let _: ThreadRealtimeStartResponse = to_response(start_response)?; + + let started = + read_notification::(&mut mcp, "thread/realtime/started") + .await?; + assert_eq!(started.thread_id, thread_start.thread.id); + assert!(started.session_id.is_some()); + + let audio_append_request_id = mcp + .send_thread_realtime_append_audio_request(ThreadRealtimeAppendAudioParams { + thread_id: started.thread_id.clone(), + audio: ThreadRealtimeAudioChunk { + data: "BQYH".to_string(), + sample_rate: 24_000, + num_channels: 1, + samples_per_channel: Some(480), + }, + }) + .await?; + let audio_append_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(audio_append_request_id)), + ) + .await??; + let _: ThreadRealtimeAppendAudioResponse = to_response(audio_append_response)?; + + let text_append_request_id = mcp + .send_thread_realtime_append_text_request(ThreadRealtimeAppendTextParams { + thread_id: started.thread_id.clone(), + text: "hello".to_string(), + }) + .await?; + let text_append_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(text_append_request_id)), + ) + .await??; + let _: ThreadRealtimeAppendTextResponse = to_response(text_append_response)?; + + let output_audio = read_notification::( + &mut mcp, + "thread/realtime/outputAudio/delta", + ) + .await?; + assert_eq!(output_audio.audio.data, "AQID"); + assert_eq!(output_audio.audio.sample_rate, 24_000); + assert_eq!(output_audio.audio.num_channels, 1); + assert_eq!(output_audio.audio.samples_per_channel, Some(512)); + + let item_added = read_notification::( + &mut mcp, + "thread/realtime/itemAdded", + ) + .await?; + assert_eq!(item_added.thread_id, output_audio.thread_id); + assert_eq!(item_added.item["type"], json!("message")); + + let realtime_error = + read_notification::(&mut mcp, "thread/realtime/error") + .await?; + assert_eq!(realtime_error.thread_id, output_audio.thread_id); + assert_eq!(realtime_error.message, "upstream boom"); + + let closed = + read_notification::(&mut mcp, "thread/realtime/closed") + .await?; + assert_eq!(closed.thread_id, output_audio.thread_id); + assert_eq!(closed.reason.as_deref(), Some("transport_closed")); + + let connections = realtime_server.connections(); + assert_eq!(connections.len(), 1); + let connection = &connections[0]; + assert_eq!(connection.len(), 3); + assert_eq!( + connection[0].body_json()["type"].as_str(), + Some("session.create") + ); + let mut request_types = [ + connection[1].body_json()["type"] + .as_str() + .context("expected websocket request type")? + .to_string(), + connection[2].body_json()["type"] + .as_str() + .context("expected websocket request type")? + .to_string(), + ]; + request_types.sort(); + assert_eq!( + request_types, + [ + "conversation.item.create".to_string(), + "response.input_audio.delta".to_string(), + ] + ); + + realtime_server.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> { + skip_if_no_network!(Ok(())); + + let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let realtime_server = start_websocket_server(vec![vec![ + vec![json!({ + "type": "session.created", + "session": { "id": "sess_backend" } + })], + vec![], + ]]) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &responses_server.uri(), + realtime_server.uri(), + true, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + mcp.initialize().await?; + + let thread_start_request_id = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_start_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_request_id)), + ) + .await??; + let thread_start: ThreadStartResponse = to_response(thread_start_response)?; + + let start_request_id = mcp + .send_thread_realtime_start_request(ThreadRealtimeStartParams { + thread_id: thread_start.thread.id.clone(), + prompt: "backend prompt".to_string(), + session_id: None, + }) + .await?; + let start_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(start_request_id)), + ) + .await??; + let _: ThreadRealtimeStartResponse = to_response(start_response)?; + + let started = + read_notification::(&mut mcp, "thread/realtime/started") + .await?; + + let stop_request_id = mcp + .send_thread_realtime_stop_request(ThreadRealtimeStopParams { + thread_id: started.thread_id.clone(), + }) + .await?; + let stop_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(stop_request_id)), + ) + .await??; + let _: ThreadRealtimeStopResponse = to_response(stop_response)?; + + let closed = + read_notification::(&mut mcp, "thread/realtime/closed") + .await?; + assert_eq!(closed.thread_id, started.thread_id); + assert!(matches!( + closed.reason.as_deref(), + Some("requested" | "transport_closed") + )); + + realtime_server.shutdown().await; + Ok(()) +} + +#[tokio::test] +async fn realtime_conversation_requires_feature_flag() -> Result<()> { + skip_if_no_network!(Ok(())); + + let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; + let realtime_server = start_websocket_server(vec![vec![]]).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &responses_server.uri(), + realtime_server.uri(), + false, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + mcp.initialize().await?; + + let thread_start_request_id = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_start_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_start_request_id)), + ) + .await??; + let thread_start: ThreadStartResponse = to_response(thread_start_response)?; + + let start_request_id = mcp + .send_thread_realtime_start_request(ThreadRealtimeStartParams { + thread_id: thread_start.thread.id.clone(), + prompt: "backend prompt".to_string(), + session_id: None, + }) + .await?; + let error = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(start_request_id)), + ) + .await??; + assert_invalid_request( + error, + format!( + "thread {} does not support realtime conversation", + thread_start.thread.id + ), + ); + + realtime_server.shutdown().await; + Ok(()) +} + +async fn read_notification(mcp: &mut McpProcess, method: &str) -> Result { + let notification = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_notification_message(method), + ) + .await??; + let params = notification + .params + .context("expected notification params to be present")?; + Ok(serde_json::from_value(params)?) +} + +fn create_config_toml( + codex_home: &Path, + responses_server_uri: &str, + realtime_server_uri: &str, + realtime_enabled: bool, +) -> std::io::Result<()> { + let realtime_feature_key = FEATURES + .iter() + .find(|spec| spec.id == Feature::RealtimeConversation) + .map(|spec| spec.key) + .unwrap_or("realtime_conversation"); + + std::fs::write( + codex_home.join("config.toml"), + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" +model_provider = "mock_provider" +experimental_realtime_ws_base_url = "{realtime_server_uri}" + +[features] +{realtime_feature_key} = {realtime_enabled} + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{responses_server_uri}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +"# + ), + ) +} + +fn assert_invalid_request(error: JSONRPCError, message: String) { + assert_eq!(error.error.code, -32600); + assert_eq!(error.error.message, message); + assert_eq!(error.error.data, None); +} diff --git a/codex-rs/app-server/tests/suite/v2/request_user_input.rs b/codex-rs/app-server/tests/suite/v2/request_user_input.rs index 4ee76bdca2..926cb9bbc6 100644 --- a/codex-rs/app-server/tests/suite/v2/request_user_input.rs +++ b/codex-rs/app-server/tests/suite/v2/request_user_input.rs @@ -123,9 +123,6 @@ sandbox_mode = "read-only" model_provider = "mock_provider" -[features] -collaboration_modes = true - [model_providers.mock_provider] name = "Mock provider for test" base_url = "{server_uri}/v1" diff --git a/codex-rs/app-server/tests/suite/v2/skill_approval.rs b/codex-rs/app-server/tests/suite/v2/skill_approval.rs deleted file mode 100644 index a86e459768..0000000000 --- a/codex-rs/app-server/tests/suite/v2/skill_approval.rs +++ /dev/null @@ -1,138 +0,0 @@ -use anyhow::Result; -use app_test_support::McpProcess; -use app_test_support::create_final_assistant_message_sse_response; -use app_test_support::create_mock_responses_server_sequence; -use app_test_support::to_response; -use app_test_support::write_mock_responses_config_toml; -use codex_app_server_protocol::JSONRPCResponse; -use codex_app_server_protocol::RequestId; -use codex_app_server_protocol::ServerRequest; -use codex_app_server_protocol::ThreadStartParams; -use codex_app_server_protocol::ThreadStartResponse; -use codex_app_server_protocol::TurnStartParams; -use codex_app_server_protocol::TurnStartResponse; -use codex_app_server_protocol::UserInput as V2UserInput; -use codex_core::features::Feature; -use core_test_support::responses; -use pretty_assertions::assert_eq; -use serde_json::json; -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; -use tokio::time::timeout; - -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); - -fn write_skill_with_script( - home: &Path, - name: &str, - script_body: &str, -) -> Result { - let skill_dir = home.join("skills").join(name); - let scripts_dir = skill_dir.join("scripts"); - fs::create_dir_all(&scripts_dir)?; - fs::write( - skill_dir.join("SKILL.md"), - format!("---\nname: {name}\ndescription: {name} skill\n---\n"), - )?; - let script_path = scripts_dir.join("run.py"); - fs::write(&script_path, script_body)?; - Ok(script_path) -} - -fn shell_command_response(tool_call_id: &str, command: &str) -> Result { - let arguments = serde_json::to_string(&json!({ - "command": command, - "timeout_ms": 500, - }))?; - Ok(responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_function_call(tool_call_id, "shell_command", &arguments), - responses::ev_completed("resp-1"), - ])) -} - -fn command_for_script(script_path: &Path) -> Result { - let runner = if cfg!(windows) { "python" } else { "python3" }; - let script_path = script_path.to_string_lossy().into_owned(); - Ok(shlex::try_join([runner, script_path.as_str()])?) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn skill_request_approval_round_trip_on_shell_command_skill_script_exec() -> Result<()> { - let codex_home = tempfile::TempDir::new()?; - let script_path = write_skill_with_script(codex_home.path(), "demo", "print('hello')")?; - let tool_call_id = "skill-call"; - let command = command_for_script(&script_path)?; - let server = create_mock_responses_server_sequence(vec![ - shell_command_response(tool_call_id, &command)?, - create_final_assistant_message_sse_response("done")?, - ]) - .await; - write_mock_responses_config_toml( - codex_home.path(), - &server.uri(), - &BTreeMap::from([(Feature::SkillApproval, true)]), - 8192, - Some(false), - "mock_provider", - "compact", - )?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let thread_start_id = mcp - .send_thread_start_request(ThreadStartParams { - model: Some("mock-model".to_string()), - ..Default::default() - }) - .await?; - let thread_start_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?; - - let turn_start_id = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), - input: vec![V2UserInput::Text { - text: "ask something".to_string(), - text_elements: Vec::new(), - }], - model: Some("mock-model".to_string()), - ..Default::default() - }) - .await?; - let turn_start_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)), - ) - .await??; - let TurnStartResponse { .. } = to_response::(turn_start_resp)?; - - let server_req = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_request_message(), - ) - .await??; - let ServerRequest::SkillRequestApproval { request_id, params } = server_req else { - panic!("expected SkillRequestApproval request, got: {server_req:?}"); - }; - - assert_eq!(params.item_id, tool_call_id); - assert_eq!(params.skill_name, "demo"); - - mcp.send_response(request_id, serde_json::json!({ "decision": "approve" })) - .await?; - - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - Ok(()) -} diff --git a/codex-rs/app-server/tests/suite/v2/thread_archive.rs b/codex-rs/app-server/tests/suite/v2/thread_archive.rs index d7aa60f2bb..20fd6fd884 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_archive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_archive.rs @@ -8,8 +8,13 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadArchiveResponse; use codex_app_server_protocol::ThreadArchivedNotification; +use codex_app_server_protocol::ThreadResumeParams; +use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::ThreadUnarchiveParams; +use codex_app_server_protocol::ThreadUnarchiveResponse; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput; @@ -155,6 +160,140 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut primary = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; + + let start_id = primary + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let start_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(start_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(start_resp)?; + + let turn_start_id = primary + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![UserInput::Text { + text: "materialize".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_start_response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(turn_start_id)), + ) + .await??; + let _: TurnStartResponse = to_response::(turn_start_response)?; + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("turn/completed"), + ) + .await??; + primary.clear_message_buffer(); + + let mut secondary = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??; + + let archive_id = primary + .send_thread_archive_request(ThreadArchiveParams { + thread_id: thread.id.clone(), + }) + .await?; + let archive_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(archive_id)), + ) + .await??; + let _: ThreadArchiveResponse = to_response::(archive_resp)?; + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("thread/archived"), + ) + .await??; + + let unarchive_id = primary + .send_thread_unarchive_request(ThreadUnarchiveParams { + thread_id: thread.id.clone(), + }) + .await?; + let unarchive_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_response_message(RequestId::Integer(unarchive_id)), + ) + .await??; + let _: ThreadUnarchiveResponse = to_response::(unarchive_resp)?; + timeout( + DEFAULT_READ_TIMEOUT, + primary.read_stream_until_notification_message("thread/unarchived"), + ) + .await??; + primary.clear_message_buffer(); + + let resume_id = secondary + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread.id.clone(), + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + secondary.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let resume: ThreadResumeResponse = to_response::(resume_resp)?; + assert_eq!(resume.thread.status, ThreadStatus::Idle); + primary.clear_message_buffer(); + secondary.clear_message_buffer(); + + let resumed_turn_id = secondary + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![UserInput::Text { + text: "secondary turn".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let resumed_turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + secondary.read_stream_until_response_message(RequestId::Integer(resumed_turn_id)), + ) + .await??; + let _: TurnStartResponse = to_response::(resumed_turn_resp)?; + + assert!( + timeout( + std::time::Duration::from_millis(250), + primary.read_stream_until_notification_message("turn/started"), + ) + .await + .is_err() + ); + + timeout( + DEFAULT_READ_TIMEOUT, + secondary.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + Ok(()) +} + fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write(config_toml, config_contents(server_uri)) diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index 1f6e0627eb..96aa2ec764 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -78,6 +78,7 @@ async fn list_threads_with_sort( source_kinds, archived, cwd: None, + search_term: None, }) .await?; let resp: JSONRPCResponse = timeout( @@ -491,6 +492,7 @@ async fn thread_list_respects_cwd_filter() -> Result<()> { source_kinds: None, archived: None, cwd: Some(target_cwd.to_string_lossy().into_owned()), + search_term: None, }) .await?; let resp: JSONRPCResponse = timeout( @@ -511,6 +513,86 @@ async fn thread_list_respects_cwd_filter() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_list_respects_search_term_filter() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + r#" +model = "mock-model" +approval_policy = "never" +suppress_unstable_features_warning = true + +[features] +sqlite = true +"#, + )?; + + let older_match = create_fake_rollout( + codex_home.path(), + "2025-01-02T10-00-00", + "2025-01-02T10:00:00Z", + "match: needle", + Some("mock_provider"), + None, + )?; + let _non_match = create_fake_rollout( + codex_home.path(), + "2025-01-02T11-00-00", + "2025-01-02T11:00:00Z", + "no hit here", + Some("mock_provider"), + None, + )?; + let newer_match = create_fake_rollout( + codex_home.path(), + "2025-01-02T12-00-00", + "2025-01-02T12:00:00Z", + "needle suffix", + Some("mock_provider"), + None, + )?; + + // `thread/list` only applies `search_term` on the sqlite path. In this test we + // create rollouts manually, so we must also create the sqlite DB and mark backfill + // complete; otherwise app-server will permanently use filesystem fallback. + let state_db = codex_state::StateRuntime::init( + codex_home.path().to_path_buf(), + "mock_provider".into(), + None, + ) + .await?; + state_db.mark_backfill_complete(None).await?; + + let mut mcp = init_mcp(codex_home.path()).await?; + let request_id = mcp + .send_thread_list_request(codex_app_server_protocol::ThreadListParams { + cursor: None, + limit: Some(10), + sort_key: None, + model_providers: Some(vec!["mock_provider".to_string()]), + source_kinds: None, + archived: None, + cwd: None, + search_term: Some("needle".to_string()), + }) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let ThreadListResponse { + data, next_cursor, .. + } = to_response::(resp)?; + + assert_eq!(next_cursor, None); + let ids: Vec<_> = data.iter().map(|thread| thread.id.as_str()).collect(); + assert_eq!(ids, vec![newer_match, older_match]); + + Ok(()) +} + #[tokio::test] async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()> { let codex_home = TempDir::new()?; @@ -1335,6 +1417,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> { source_kinds: None, archived: None, cwd: None, + search_term: None, }) .await?; let error: JSONRPCError = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 384aad6c86..5d2211d3c8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -289,6 +289,7 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> { source_kinds: None, archived: None, cwd: None, + search_term: None, }) .await?; let list_resp: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 4e63d4c546..c0b84fe42f 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -146,6 +146,34 @@ model_reasoning_effort = "high" Ok(()) } +#[tokio::test] +async fn thread_start_accepts_metrics_service_name() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let req_id = mcp + .send_thread_start_request(ThreadStartParams { + service_name: Some("my_app_server_client".to_string()), + ..Default::default() + }) + .await?; + + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(resp)?; + assert!(!thread.id.is_empty(), "thread id should not be empty"); + + Ok(()) +} + #[tokio::test] async fn thread_start_ephemeral_remains_pathless() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs new file mode 100644 index 0000000000..f16ef8bb06 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs @@ -0,0 +1,383 @@ +use anyhow::Context; +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::create_mock_responses_server_sequence; +use app_test_support::create_shell_command_sse_response; +use app_test_support::to_response; +use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCNotification; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadLoadedListParams; +use codex_app_server_protocol::ThreadLoadedListResponse; +use codex_app_server_protocol::ThreadReadParams; +use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadResumeParams; +use codex_app_server_protocol::ThreadResumeResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::ThreadStatusChangedNotification; +use codex_app_server_protocol::ThreadUnsubscribeParams; +use codex_app_server_protocol::ThreadUnsubscribeResponse; +use codex_app_server_protocol::ThreadUnsubscribeStatus; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use core_test_support::responses; +use pretty_assertions::assert_eq; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + +#[tokio::test] +async fn thread_unsubscribe_unloads_thread_and_emits_thread_closed_notification() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + + let unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { + thread_id: thread_id.clone(), + }) + .await?; + let unsubscribe_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)), + ) + .await??; + let unsubscribe = to_response::(unsubscribe_resp)?; + assert_eq!(unsubscribe.status, ThreadUnsubscribeStatus::Unsubscribed); + + let closed_notif: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/closed"), + ) + .await??; + let parsed: ServerNotification = closed_notif.try_into()?; + let ServerNotification::ThreadClosed(payload) = parsed else { + anyhow::bail!("expected thread/closed notification"); + }; + assert_eq!(payload.thread_id, thread_id); + + let status_changed = wait_for_thread_status_not_loaded(&mut mcp, &payload.thread_id).await?; + assert_eq!(status_changed.thread_id, payload.thread_id); + assert_eq!(status_changed.status, ThreadStatus::NotLoaded); + + let list_id = mcp + .send_thread_loaded_list_request(ThreadLoadedListParams::default()) + .await?; + let list_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(list_id)), + ) + .await??; + let ThreadLoadedListResponse { data, next_cursor } = + to_response::(list_resp)?; + assert_eq!(data, Vec::::new()); + assert_eq!(next_cursor, None); + + Ok(()) +} + +#[tokio::test] +async fn thread_unsubscribe_during_turn_interrupts_turn_and_emits_thread_closed() -> Result<()> { + #[cfg(target_os = "windows")] + let shell_command = vec![ + "powershell".to_string(), + "-Command".to_string(), + "Start-Sleep -Seconds 10".to_string(), + ]; + #[cfg(not(target_os = "windows"))] + let shell_command = vec!["sleep".to_string(), "10".to_string()]; + + let tmp = TempDir::new()?; + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir(&codex_home)?; + let working_directory = tmp.path().join("workdir"); + std::fs::create_dir(&working_directory)?; + + let server = create_mock_responses_server_sequence(vec![create_shell_command_sse_response( + shell_command.clone(), + Some(&working_directory), + Some(10_000), + "call_sleep", + )?]) + .await; + create_config_toml(&codex_home, &server.uri())?; + + let mut mcp = McpProcess::new(&codex_home).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread_id.clone(), + input: vec![V2UserInput::Text { + text: "run sleep".to_string(), + text_elements: Vec::new(), + }], + cwd: Some(working_directory), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _: TurnStartResponse = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + wait_for_command_execution_item_started(&mut mcp), + ) + .await??; + + let unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { + thread_id: thread_id.clone(), + }) + .await?; + let unsubscribe_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)), + ) + .await??; + let unsubscribe = to_response::(unsubscribe_resp)?; + assert_eq!(unsubscribe.status, ThreadUnsubscribeStatus::Unsubscribed); + + let closed_notif: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/closed"), + ) + .await??; + let parsed: ServerNotification = closed_notif.try_into()?; + let ServerNotification::ThreadClosed(payload) = parsed else { + anyhow::bail!("expected thread/closed notification"); + }; + assert_eq!(payload.thread_id, thread_id); + + Ok(()) +} + +#[tokio::test] +async fn thread_unsubscribe_clears_cached_status_before_resume() -> Result<()> { + let server = responses::start_mock_server().await; + let _response_mock = responses::mount_sse_once( + &server, + responses::sse_failed("resp-1", "server_error", "simulated failure"), + ) + .await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread_id.clone(), + input: vec![V2UserInput::Text { + text: "fail this turn".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _: TurnStartResponse = to_response::(turn_resp)?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("error"), + ) + .await??; + + let read_id = mcp + .send_thread_read_request(ThreadReadParams { + thread_id: thread_id.clone(), + include_turns: false, + }) + .await?; + let read_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_id)), + ) + .await??; + let ThreadReadResponse { thread } = to_response::(read_resp)?; + assert_eq!(thread.status, ThreadStatus::SystemError); + + let unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { + thread_id: thread_id.clone(), + }) + .await?; + let unsubscribe_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)), + ) + .await??; + let unsubscribe = to_response::(unsubscribe_resp)?; + assert_eq!(unsubscribe.status, ThreadUnsubscribeStatus::Unsubscribed); + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/closed"), + ) + .await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id, + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let resume: ThreadResumeResponse = to_response::(resume_resp)?; + assert_eq!(resume.thread.status, ThreadStatus::Idle); + + Ok(()) +} + +#[tokio::test] +async fn thread_unsubscribe_reports_not_loaded_after_thread_is_unloaded() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_id = start_thread(&mut mcp).await?; + + let first_unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { + thread_id: thread_id.clone(), + }) + .await?; + let first_unsubscribe_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(first_unsubscribe_id)), + ) + .await??; + let first_unsubscribe = to_response::(first_unsubscribe_resp)?; + assert_eq!( + first_unsubscribe.status, + ThreadUnsubscribeStatus::Unsubscribed + ); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/closed"), + ) + .await??; + + let second_unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { thread_id }) + .await?; + let second_unsubscribe_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(second_unsubscribe_id)), + ) + .await??; + let second_unsubscribe = to_response::(second_unsubscribe_resp)?; + assert_eq!( + second_unsubscribe.status, + ThreadUnsubscribeStatus::NotLoaded + ); + + Ok(()) +} + +async fn wait_for_command_execution_item_started(mcp: &mut McpProcess) -> Result<()> { + loop { + let started_notif = mcp + .read_stream_until_notification_message("item/started") + .await?; + let started_params = started_notif.params.context("item/started params")?; + let started: ItemStartedNotification = serde_json::from_value(started_params)?; + if let ThreadItem::CommandExecution { .. } = started.item { + return Ok(()); + } + } +} + +async fn wait_for_thread_status_not_loaded( + mcp: &mut McpProcess, + thread_id: &str, +) -> Result { + loop { + let status_changed_notif: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("thread/status/changed"), + ) + .await??; + let status_changed_params = status_changed_notif + .params + .context("thread/status/changed params must be present")?; + let status_changed: ThreadStatusChangedNotification = + serde_json::from_value(status_changed_params)?; + if status_changed.thread_id == thread_id && status_changed.status == ThreadStatus::NotLoaded + { + return Ok(status_changed); + } + } +} + +fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> { + let config_toml = codex_home.join("config.toml"); + std::fs::write( + config_toml, + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "danger-full-access" + +model_provider = "mock_provider" + +[model_providers.mock_provider] +name = "Mock provider for test" +base_url = "{server_uri}/v1" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +"# + ), + ) +} + +async fn start_thread(mcp: &mut McpProcess) -> Result { + let req_id = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(resp)?; + Ok(thread.id) +} diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index eaa017d6ae..f396aa8c44 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -9,6 +9,8 @@ use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::format_with_current_shell_display; use app_test_support::to_response; +use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; +use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::ByteRange; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::CommandExecutionApprovalDecision; @@ -19,6 +21,7 @@ use codex_app_server_protocol::FileChangeOutputDeltaNotification; use codex_app_server_protocol::FileChangeRequestApprovalResponse; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::PatchApplyStatus; @@ -45,10 +48,13 @@ use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use serde_json::json; use std::collections::BTreeMap; +use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; @@ -221,6 +227,143 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::from([(Feature::Personality, true)]), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![ + V2UserInput::Text { + text: "x".repeat(MAX_USER_INPUT_TEXT_CHARS), + text_elements: Vec::new(), + }, + V2UserInput::Mention { + name: "Demo App".to_string(), + path: "app://demo-app".to_string(), + }, + ], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + assert_eq!(turn.status, TurnStatus::InProgress); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + Ok(()) +} + +#[tokio::test] +async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + "http://localhost/unused", + "never", + &BTreeMap::from([(Feature::Personality, true)]), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let first = "x".repeat(MAX_USER_INPUT_TEXT_CHARS / 2); + let second = "y".repeat(MAX_USER_INPUT_TEXT_CHARS / 2 + 1); + let actual_chars = first.chars().count() + second.chars().count(); + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![ + V2UserInput::Text { + text: first, + text_elements: Vec::new(), + }, + V2UserInput::Text { + text: second, + text_elements: Vec::new(), + }, + ], + ..Default::default() + }) + .await?; + let err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(turn_req)), + ) + .await??; + + assert_eq!(err.error.code, INVALID_PARAMS_ERROR_CODE); + assert_eq!( + err.error.message, + format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.") + ); + let data = err.error.data.expect("expected structured error data"); + assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE); + assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS); + assert_eq!(data["actual_chars"], actual_chars); + + let turn_started = tokio::time::timeout( + std::time::Duration::from_millis(250), + mcp.read_stream_until_notification_message("turn/started"), + ) + .await; + assert!( + turn_started.is_err(), + "did not expect a turn/started notification for rejected input" + ); + + Ok(()) +} + #[tokio::test] async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<()> { // Provide a mock server and config so model wiring is valid. @@ -352,7 +495,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { codex_home.path(), &server.uri(), "never", - &BTreeMap::default(), + &BTreeMap::from([(Feature::DefaultModeRequestUserInput, true)]), )?; let mut mcp = McpProcess::new(codex_home.path()).await?; @@ -412,7 +555,92 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let payload = request.body_json(); assert_eq!(payload["model"].as_str(), Some("mock-model-collab")); let payload_text = payload.to_string(); - assert!(payload_text.contains("The `request_user_input` tool is unavailable in Default mode.")); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); + + Ok(()) +} + +#[tokio::test] +async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instructions_v2() +-> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("gpt-5.2-codex".to_string()), + config: Some(HashMap::from([( + "features.default_mode_request_user_input".to_string(), + json!(true), + )])), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Default, + settings: Settings { + model: "mock-model-collab".to_string(), + reasoning_effort: Some(ReasoningEffort::High), + developer_instructions: None, + }, + }; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + model: Some("mock-model-override".to_string()), + effort: Some(ReasoningEffort::Low), + summary: Some(ReasoningSummary::Auto), + output_schema: None, + collaboration_mode: Some(collaboration_mode), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _turn: TurnStartResponse = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let payload_text = request.body_json().to_string(); + assert!(payload_text.contains("The `request_user_input` tool is available in Default mode.")); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index 340a5e156c..bdbb283432 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -13,6 +13,7 @@ use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::to_response; +use codex_app_server_protocol::CommandAction; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::CommandExecutionStatus; @@ -542,7 +543,10 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() CommandExecutionApprovalDecision::Cancel, ]; let mut target_decision_index = 0; - while target_decision_index < target_decisions.len() { + let first_file_str = first_file.to_string_lossy().into_owned(); + let second_file_str = second_file.to_string_lossy().into_owned(); + let parent_shell_hint = format!("&& {}", &first_file_str); + while target_decision_index < target_decisions.len() || !saw_parent_approval { let server_req = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_request_message(), @@ -558,16 +562,21 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() .command .as_deref() .expect("approval command should be present"); - let is_target_subcommand = (approval_command.starts_with("/bin/rm ") - || approval_command.starts_with("/usr/bin/rm ")) - && (approval_command.contains(&first_file.display().to_string()) - || approval_command.contains(&second_file.display().to_string())); + let has_first_file = approval_command.contains(&first_file_str); + let has_second_file = approval_command.contains(&second_file_str); + let mentions_rm_binary = + approval_command.contains("/bin/rm ") || approval_command.contains("/usr/bin/rm "); + let has_rm_action = params.command_actions.as_ref().is_some_and(|actions| { + actions.iter().any(|action| match action { + CommandAction::Read { name, .. } => name == "rm", + CommandAction::Unknown { command } => command.contains("rm"), + _ => false, + }) + }); + let is_target_subcommand = + (has_first_file != has_second_file) && (has_rm_action || mentions_rm_binary); + if is_target_subcommand { - assert!( - approval_command.contains(&first_file.display().to_string()) - || approval_command.contains(&second_file.display().to_string()), - "expected zsh subcommand approval for one of the rm commands, got: {approval_command}" - ); approved_subcommand_ids.push( params .approval_id @@ -577,7 +586,9 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() approved_subcommand_strings.push(approval_command.to_string()); } let is_parent_approval = approval_command.contains(&zsh_path.display().to_string()) - && approval_command.contains(&shell_command); + && (approval_command.contains(&shell_command) + || (has_first_file && has_second_file) + || approval_command.contains(&parent_shell_hint)); let decision = if is_target_subcommand { let decision = target_decisions[target_decision_index].clone(); target_decision_index += 1; diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index 779e77577b..61a8a9794c 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -6,6 +6,8 @@ use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::to_response; +use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; +use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; @@ -17,6 +19,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput as V2UserInput; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use tempfile::TempDir; use tokio::time::timeout; @@ -67,6 +70,109 @@ async fn turn_steer_requires_active_turn() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_steer_rejects_oversized_text_input() -> Result<()> { + #[cfg(target_os = "windows")] + let shell_command = vec![ + "powershell".to_string(), + "-Command".to_string(), + "Start-Sleep -Seconds 10".to_string(), + ]; + #[cfg(not(target_os = "windows"))] + let shell_command = vec!["sleep".to_string(), "10".to_string()]; + + let tmp = TempDir::new()?; + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir(&codex_home)?; + let working_directory = tmp.path().join("workdir"); + std::fs::create_dir(&working_directory)?; + + let server = + create_mock_responses_server_sequence_unchecked(vec![create_shell_command_sse_response( + shell_command.clone(), + Some(&working_directory), + Some(10_000), + "call_sleep", + )?]) + .await; + create_config_toml(&codex_home, &server.uri())?; + + let mut mcp = McpProcess::new(&codex_home).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "run sleep".to_string(), + text_elements: Vec::new(), + }], + cwd: Some(working_directory.clone()), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + + let _task_started: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("codex/event/task_started"), + ) + .await??; + + let oversized_input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); + let steer_req = mcp + .send_turn_steer_request(TurnSteerParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: oversized_input.clone(), + text_elements: Vec::new(), + }], + expected_turn_id: turn.id.clone(), + }) + .await?; + let steer_err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(steer_req)), + ) + .await??; + + assert_eq!(steer_err.error.code, INVALID_PARAMS_ERROR_CODE); + assert_eq!( + steer_err.error.message, + format!("Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters.") + ); + let data = steer_err + .error + .data + .expect("expected structured error data"); + assert_eq!(data["input_error_code"], INPUT_TOO_LARGE_ERROR_CODE); + assert_eq!(data["max_chars"], MAX_USER_INPUT_TEXT_CHARS); + assert_eq!(data["actual_chars"], oversized_input.chars().count()); + + mcp.interrupt_turn_and_wait_for_aborted(thread.id, turn.id, DEFAULT_READ_TIMEOUT) + .await?; + + Ok(()) +} + #[tokio::test] async fn turn_steer_returns_active_turn_id() -> Result<()> { #[cfg(target_os = "windows")] diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index fca681d5ea..d8dbaaa30d 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -17,19 +17,31 @@ const EXECVE_WRAPPER_ARG0: &str = "codex-execve-wrapper"; const LOCK_FILENAME: &str = ".lock"; const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024; +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Arg0DispatchPaths { + pub codex_linux_sandbox_exe: Option, + pub main_execve_wrapper_exe: Option, +} + /// Keeps the per-session PATH entry alive and locked for the process lifetime. pub struct Arg0PathEntryGuard { _temp_dir: TempDir, _lock_file: File, + paths: Arg0DispatchPaths, } impl Arg0PathEntryGuard { - fn new(temp_dir: TempDir, lock_file: File) -> Self { + fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self { Self { _temp_dir: temp_dir, _lock_file: lock_file, + paths, } } + + pub fn paths(&self) -> &Arg0DispatchPaths { + &self.paths + } } pub fn arg0_dispatch() -> Option { @@ -124,33 +136,43 @@ pub fn arg0_dispatch() -> Option { /// 3. Derive the path to the current executable (so children can re-invoke the /// sandbox) when running on Linux. /// 4. Execute the provided async `main_fn` inside that runtime, forwarding any -/// error. Note that `main_fn` receives `codex_linux_sandbox_exe: -/// Option`, as an argument, which is generally needed as part of -/// constructing [`codex_core::config::Config`]. +/// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which +/// contains the helper executable paths needed to construct +/// [`codex_core::config::Config`]. /// /// This function should be used to wrap any `main()` function in binary crates /// in this workspace that depends on these helper CLIs. pub fn arg0_dispatch_or_else(main_fn: F) -> anyhow::Result<()> where - F: FnOnce(Option) -> Fut, + F: FnOnce(Arg0DispatchPaths) -> Fut, Fut: Future>, { // Retain the TempDir so it exists for the lifetime of the invocation of // this executable. Admittedly, we could invoke `keep()` on it, but it // would be nice to avoid leaving temporary directories behind, if possible. - let _path_entry = arg0_dispatch(); + let path_entry = arg0_dispatch(); // Regular invocation – create a Tokio runtime and execute the provided // async entry-point. let runtime = build_runtime()?; runtime.block_on(async move { - let codex_linux_sandbox_exe: Option = if cfg!(target_os = "linux") { - std::env::current_exe().ok() - } else { - None + let current_exe = std::env::current_exe().ok(); + let paths = Arg0DispatchPaths { + codex_linux_sandbox_exe: if cfg!(target_os = "linux") { + current_exe.or_else(|| { + path_entry + .as_ref() + .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone()) + }) + } else { + None + }, + main_execve_wrapper_exe: path_entry + .as_ref() + .and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()), }; - main_fn(codex_linux_sandbox_exe).await + main_fn(paths).await }) } @@ -301,7 +323,30 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result std::io::Result<()> { diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index a3470ff1b9..620a93872e 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -298,12 +298,7 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { .as_deref() .map(|value| !value.trim().is_empty()) .unwrap_or(false); - let existing_description_present = existing - .description - .as_deref() - .map(|value| !value.trim().is_empty()) - .unwrap_or(false); - if !existing_description_present && incoming_description_present { + if incoming_description_present { existing.description = description; } diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a65fa486e4..3fd4350826 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::NetworkProxyAuditMetadata; use codex_core::exec_env::create_env; use codex_core::landlock::spawn_command_under_linux_sandbox; #[cfg(target_os = "macos")] @@ -223,6 +224,7 @@ async fn run_command_under_sandbox( None, None, managed_network_requirements_enabled, + NetworkProxyAuditMetadata::default(), ) .await .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?, diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 42622f25d3..f2fd5a3bdc 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -22,7 +22,7 @@ const LOGIN_SUCCESS_MESSAGE: &str = "Successfully logged in"; fn print_login_server_start(actual_port: u16, auth_url: &str) { eprintln!( - "Starting local login server on http://localhost:{actual_port}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}" + "Starting local login server on http://localhost:{actual_port}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}\n\nOn a remote or headless machine? Use `codex login --device-auth` instead." ); } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index f8a8a3a6fb..c271fe0b9e 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -3,6 +3,7 @@ use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; +use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_chatgpt::apply_command::ApplyCommand; use codex_chatgpt::apply_command::run_apply_command; @@ -543,13 +544,13 @@ fn stage_str(stage: codex_core::features::Stage) -> &'static str { } fn main() -> anyhow::Result<()> { - arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { - cli_main(codex_linux_sandbox_exe).await?; + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { + cli_main(arg0_paths).await?; Ok(()) }) } -async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { +async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { let MultitoolCli { config_overrides: mut root_config_overrides, feature_toggles, @@ -567,7 +568,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut interactive.config_overrides, root_config_overrides.clone(), ); - let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; + let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Exec(mut exec_cli)) => { @@ -575,7 +576,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut exec_cli.config_overrides, root_config_overrides.clone(), ); - codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; + codex_exec::run_main(exec_cli, arg0_paths.clone()).await?; } Some(Subcommand::Review(review_args)) => { let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?; @@ -584,10 +585,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut exec_cli.config_overrides, root_config_overrides.clone(), ); - codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; + codex_exec::run_main(exec_cli, arg0_paths.clone()).await?; } Some(Subcommand::McpServer) => { - codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?; + codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?; } Some(Subcommand::Mcp(mut mcp_cli)) => { // Propagate any root-level config overrides (e.g. `-c key=value`). @@ -598,7 +599,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() None => { let transport = app_server_cli.listen; codex_app_server::run_main_with_transport( - codex_linux_sandbox_exe, + arg0_paths.clone(), root_config_overrides, codex_core::config_loader::LoaderOverrides::default(), app_server_cli.analytics_default_enabled, @@ -642,7 +643,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() all, config_overrides, ); - let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; + let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Fork(ForkCommand { @@ -659,7 +660,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() all, config_overrides, ); - let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?; + let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?; handle_app_exit(exit_info)?; } Some(Subcommand::Login(mut login_cli)) => { @@ -708,7 +709,8 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut cloud_cli.config_overrides, root_config_overrides.clone(), ); - codex_cloud_tasks::run_main(cloud_cli, codex_linux_sandbox_exe).await?; + codex_cloud_tasks::run_main(cloud_cli, arg0_paths.codex_linux_sandbox_exe.clone()) + .await?; } Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd { SandboxCommand::Macos(mut seatbelt_cli) => { @@ -718,7 +720,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); codex_cli::debug_sandbox::run_command_under_seatbelt( seatbelt_cli, - codex_linux_sandbox_exe, + arg0_paths.codex_linux_sandbox_exe.clone(), ) .await?; } @@ -729,7 +731,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); codex_cli::debug_sandbox::run_command_under_landlock( landlock_cli, - codex_linux_sandbox_exe, + arg0_paths.codex_linux_sandbox_exe.clone(), ) .await?; } @@ -740,7 +742,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); codex_cli::debug_sandbox::run_command_under_windows( windows_cli, - codex_linux_sandbox_exe, + arg0_paths.codex_linux_sandbox_exe.clone(), ) .await?; } @@ -806,6 +808,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() stage_width = stage_width.max(stage.len()); rows.push((name, stage, enabled)); } + rows.sort_unstable_by_key(|(name, _, _)| *name); for (name, stage, enabled) in rows { println!("{name:, + arg0_paths: Arg0DispatchPaths, ) -> std::io::Result { if let Some(prompt) = interactive.prompt.take() { // Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state. @@ -912,7 +915,7 @@ async fn run_interactive_tui( } } - codex_tui::run_main(interactive, codex_linux_sandbox_exe).await + codex_tui::run_main(interactive, arg0_paths).await } fn confirm(prompt: &str) -> std::io::Result { diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 396e077411..2da868cada 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -250,6 +250,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }; servers.insert(name.clone(), new_entry); @@ -272,6 +273,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re oauth_config.http_headers, oauth_config.env_http_headers, &Vec::new(), + None, config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), ) @@ -356,6 +358,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) http_headers, env_http_headers, &scopes, + server.oauth_resource.as_deref(), config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), ) diff --git a/codex-rs/cli/tests/features.rs b/codex-rs/cli/tests/features.rs index 6b2ed72f8a..17a7eff679 100644 --- a/codex-rs/cli/tests/features.rs +++ b/codex-rs/cli/tests/features.rs @@ -2,6 +2,7 @@ use std::path::Path; use anyhow::Result; use predicates::str::contains; +use pretty_assertions::assert_eq; use tempfile::TempDir; fn codex_command(codex_home: &Path) -> Result { @@ -58,3 +59,33 @@ async fn features_enable_under_development_feature_prints_warning() -> Result<() Ok(()) } + +#[tokio::test] +async fn features_list_is_sorted_alphabetically_by_feature_name() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut cmd = codex_command(codex_home.path())?; + let output = cmd + .args(["features", "list"]) + .assert() + .success() + .get_output() + .stdout + .clone(); + let stdout = String::from_utf8(output)?; + + let actual_names = stdout + .lines() + .map(|line| { + line.split_once(" ") + .map(|(name, _)| name.trim_end().to_string()) + .expect("feature list output should contain aligned columns") + }) + .collect::>(); + let mut expected_names = actual_names.clone(); + expected_names.sort(); + + assert_eq!(actual_names, expected_names); + + Ok(()) +} diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 6f6bf3b6de..d89ff53524 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -28,17 +28,21 @@ use serde::Serialize; use sha2::Sha256; use std::path::PathBuf; use std::sync::Arc; +use std::sync::Mutex; +use std::sync::OnceLock; use std::time::Duration; use std::time::Instant; use thiserror::Error; use tokio::fs; +use tokio::task::JoinHandle; use tokio::time::sleep; use tokio::time::timeout; const CLOUD_REQUIREMENTS_TIMEOUT: Duration = Duration::from_secs(15); const CLOUD_REQUIREMENTS_MAX_ATTEMPTS: usize = 5; const CLOUD_REQUIREMENTS_CACHE_FILENAME: &str = "cloud-requirements-cache.json"; -const CLOUD_REQUIREMENTS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); +const CLOUD_REQUIREMENTS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(5 * 60); +const CLOUD_REQUIREMENTS_CACHE_TTL: Duration = Duration::from_secs(30 * 60); const CLOUD_REQUIREMENTS_CACHE_WRITE_HMAC_KEY: &[u8] = b"codex-cloud-requirements-cache-v3-064f8542-75b4-494c-a294-97d3ce597271"; const CLOUD_REQUIREMENTS_CACHE_READ_HMAC_KEYS: &[&[u8]] = @@ -46,6 +50,11 @@ const CLOUD_REQUIREMENTS_CACHE_READ_HMAC_KEYS: &[&[u8]] = type HmacSha256 = Hmac; +fn refresher_task_slot() -> &'static Mutex>> { + static REFRESHER_TASK: OnceLock>>> = OnceLock::new(); + REFRESHER_TASK.get_or_init(|| Mutex::new(None)) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum FetchCloudRequirementsStatus { BackendClientInit, @@ -188,6 +197,7 @@ impl RequirementsFetcher for BackendRequirementsFetcher { } } +#[derive(Clone)] struct CloudRequirementsService { auth_manager: Arc, fetcher: Arc, @@ -325,6 +335,54 @@ impl CloudRequirementsService { None } + async fn refresh_cache_in_background(&self) { + loop { + sleep(CLOUD_REQUIREMENTS_CACHE_REFRESH_INTERVAL).await; + match timeout(self.timeout, self.refresh_cache()).await { + Ok(true) => {} + Ok(false) => break, + Err(_) => { + tracing::warn!( + "Timed out refreshing cloud requirements cache from remote; keeping existing cache" + ); + } + } + } + } + + async fn refresh_cache(&self) -> bool { + let Some(auth) = self.auth_manager.auth().await else { + return false; + }; + if !auth.is_chatgpt_auth() + || !matches!( + auth.account_plan_type(), + Some(PlanType::Business | PlanType::Enterprise) + ) + { + return false; + } + + let token_data = auth.get_token_data().ok(); + let chatgpt_user_id = token_data + .as_ref() + .and_then(|token_data| token_data.id_token.chatgpt_user_id.as_deref()); + let account_id = auth.get_account_id(); + let account_id = account_id.as_deref(); + + if self + .fetch_with_retries(&auth, chatgpt_user_id, account_id) + .await + .is_none() + { + tracing::warn!( + path = %self.cache_path.display(), + "Failed to refresh cloud requirements cache from remote" + ); + } + true + } + async fn load_cache( &self, chatgpt_user_id: Option<&str>, @@ -452,7 +510,17 @@ pub fn cloud_requirements_loader( codex_home, CLOUD_REQUIREMENTS_TIMEOUT, ); + let refresh_service = service.clone(); let task = tokio::spawn(async move { service.fetch_with_timeout().await }); + let refresh_task = + tokio::spawn(async move { refresh_service.refresh_cache_in_background().await }); + let mut refresher_guard = refresher_task_slot().lock().unwrap_or_else(|err| { + tracing::warn!("cloud requirements refresher task slot was poisoned"); + err.into_inner() + }); + if let Some(existing_task) = refresher_guard.replace(refresh_task) { + existing_task.abort(); + } CloudRequirementsLoader::new(async move { task.await .inspect_err(|err| tracing::warn!(error = %err, "Cloud requirements task failed")) @@ -1052,7 +1120,11 @@ mod tests { let cache_file: CloudRequirementsCacheFile = serde_json::from_str(&std::fs::read_to_string(path).expect("read cache")) .expect("parse cache"); - assert!(cache_file.signed_payload.expires_at > Utc::now()); + assert!( + cache_file.signed_payload.expires_at + <= cache_file.signed_payload.cached_at + ChronoDuration::minutes(30) + ); + assert!(cache_file.signed_payload.expires_at > cache_file.signed_payload.cached_at); assert!(cache_file.signed_payload.cached_at <= Utc::now()); assert_eq!( cache_file.signed_payload.chatgpt_user_id, @@ -1130,4 +1202,57 @@ mod tests { CLOUD_REQUIREMENTS_MAX_ATTEMPTS ); } + + #[tokio::test] + async fn refresh_from_remote_updates_cached_cloud_requirements() { + let codex_home = tempdir().expect("tempdir"); + let fetcher = Arc::new(SequenceFetcher::new(vec![ + Ok(Some("allowed_approval_policies = [\"never\"]".to_string())), + Ok(Some( + "allowed_approval_policies = [\"on-request\"]".to_string(), + )), + ])); + let service = CloudRequirementsService::new( + auth_manager_with_plan("business"), + fetcher, + codex_home.path().to_path_buf(), + CLOUD_REQUIREMENTS_TIMEOUT, + ); + + assert_eq!( + service.fetch().await, + Some(ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::Never]), + allowed_sandbox_modes: None, + allowed_web_search_modes: None, + mcp_servers: None, + rules: None, + enforce_residency: None, + network: None, + }) + ); + + service.refresh_cache().await; + + let path = codex_home.path().join(CLOUD_REQUIREMENTS_CACHE_FILENAME); + let cache_file: CloudRequirementsCacheFile = + serde_json::from_str(&std::fs::read_to_string(path).expect("read cache")) + .expect("parse cache"); + assert_eq!( + cache_file + .signed_payload + .contents + .as_deref() + .and_then(|contents| parse_cloud_requirements(contents).ok().flatten()), + Some(ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), + allowed_sandbox_modes: None, + allowed_web_search_modes: None, + mcp_servers: None, + rules: None, + enforce_residency: None, + network: None, + }) + ); + } } diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index 48cc1d7002..97d0b9e320 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -25,6 +25,8 @@ use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::tungstenite::Error as WsError; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tracing::debug; +use tracing::error; use tracing::info; use tracing::trace; use tungstenite::protocol::WebSocketConfig; @@ -62,15 +64,23 @@ impl WsStream { }; match command { WsCommand::Send { message, tx_result } => { + debug!("realtime websocket sending message"); let result = inner.send(message).await; let should_break = result.is_err(); + if let Err(err) = &result { + error!("realtime websocket send failed: {err}"); + } let _ = tx_result.send(result); if should_break { break; } } WsCommand::Close { tx_result } => { + info!("realtime websocket sending close"); let result = inner.close(None).await; + if let Err(err) = &result { + error!("realtime websocket close failed: {err}"); + } let _ = tx_result.send(result); break; } @@ -82,7 +92,9 @@ impl WsStream { }; match message { Ok(Message::Ping(payload)) => { + trace!(payload_len = payload.len(), "realtime websocket received ping"); if let Err(err) = inner.send(Message::Pong(payload)).await { + error!("realtime websocket failed to send pong: {err}"); let _ = tx_message.send(Err(err)); break; } @@ -93,6 +105,24 @@ impl WsStream { | Message::Close(_) | Message::Frame(_))) => { let is_close = matches!(message, Message::Close(_)); + match &message { + Message::Text(_) => trace!("realtime websocket received text frame"), + Message::Binary(binary) => { + error!( + payload_len = binary.len(), + "realtime websocket received unexpected binary frame" + ); + } + Message::Close(frame) => info!( + "realtime websocket received close frame: code={:?} reason={:?}", + frame.as_ref().map(|frame| frame.code), + frame.as_ref().map(|frame| frame.reason.as_str()) + ), + Message::Frame(_) => { + trace!("realtime websocket received raw frame"); + } + Message::Ping(_) | Message::Pong(_) => {} + } if tx_message.send(Ok(message)).is_err() { break; } @@ -101,6 +131,7 @@ impl WsStream { } } Err(err) => { + error!("realtime websocket receive failed: {err}"); let _ = tx_message.send(Err(err)); break; } @@ -108,6 +139,7 @@ impl WsStream { } } } + info!("realtime websocket pump exiting"); }); ( @@ -298,7 +330,7 @@ impl RealtimeWebsocketWriter { async fn send_json(&self, message: RealtimeOutboundMessage) -> Result<(), ApiError> { let payload = serde_json::to_string(&message) .map_err(|err| ApiError::Stream(format!("failed to encode realtime request: {err}")))?; - trace!("realtime websocket request: {payload}"); + debug!(?message, "realtime websocket request"); if self.is_closed.load(Ordering::SeqCst) { return Err(ApiError::Stream( @@ -325,12 +357,14 @@ impl RealtimeWebsocketEvents { Some(Ok(msg)) => msg, Some(Err(err)) => { self.is_closed.store(true, Ordering::SeqCst); + error!("realtime websocket read failed: {err}"); return Err(ApiError::Stream(format!( "failed to read websocket message: {err}" ))); } None => { self.is_closed.store(true, Ordering::SeqCst); + info!("realtime websocket event stream ended"); return Ok(None); } }; @@ -338,11 +372,18 @@ impl RealtimeWebsocketEvents { match msg { Message::Text(text) => { if let Some(event) = parse_realtime_event(&text) { + debug!(?event, "realtime websocket parsed event"); return Ok(Some(event)); } + debug!("realtime websocket ignored unsupported text frame"); } - Message::Close(_) => { + Message::Close(frame) => { self.is_closed.store(true, Ordering::SeqCst); + info!( + "realtime websocket closed: code={:?} reason={:?}", + frame.as_ref().map(|frame| frame.code), + frame.as_ref().map(|frame| frame.reason.as_str()) + ); return Ok(None); } Message::Binary(_) => { @@ -383,15 +424,24 @@ impl RealtimeWebsocketClient { request.headers_mut().extend(headers); info!("connecting realtime websocket: {ws_url}"); - let (stream, _) = + let (stream, response) = tokio_tungstenite::connect_async_with_config(request, Some(websocket_config()), false) .await .map_err(|err| { ApiError::Stream(format!("failed to connect realtime websocket: {err}")) })?; + info!( + ws_url = %ws_url, + status = %response.status(), + "realtime websocket connected" + ); let (stream, rx_message) = WsStream::new(stream); let connection = RealtimeWebsocketConnection::new(stream, rx_message); + debug!( + conversation_id = config.session_id.as_deref().unwrap_or(""), + "realtime websocket sending session.create" + ); connection .send_session_create(config.prompt, config.session_id) .await?; diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index bdd32fbd5a..925f7d52d0 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -164,6 +164,8 @@ const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state"; const X_MODELS_ETAG_HEADER: &str = "x-models-etag"; const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included"; const OPENAI_MODEL_HEADER: &str = "openai-model"; +const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE: &str = "websocket_connection_limit_reached"; +const WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE: &str = "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue."; pub struct ResponsesWebsocketConnection { stream: Arc>>, @@ -417,6 +419,12 @@ fn map_ws_error(err: WsError, url: &Url) -> ApiError { } } +#[derive(Debug, Deserialize)] +struct WrappedWebsocketError { + code: Option, + message: Option, +} + #[derive(Debug, Deserialize)] struct WrappedWebsocketErrorEvent { #[serde(rename = "type")] @@ -424,7 +432,7 @@ struct WrappedWebsocketErrorEvent { #[serde(alias = "status_code")] status: Option, #[serde(default)] - error: Option, + error: Option, #[serde(default)] headers: Option>, } @@ -437,7 +445,10 @@ fn parse_wrapped_websocket_error_event(payload: &str) -> Option Option { +fn map_wrapped_websocket_error_event( + event: WrappedWebsocketErrorEvent, + original_payload: String, +) -> Option { let WrappedWebsocketErrorEvent { status, error, @@ -445,28 +456,29 @@ fn map_wrapped_websocket_error_event(event: WrappedWebsocketErrorEvent) -> Optio .. } = event; + if let Some(error) = error.as_ref() + && let Some(code) = error.code.as_deref() + && code == WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE + { + return Some(ApiError::Retryable { + message: error + .message + .clone() + .unwrap_or_else(|| WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE.to_string()), + delay: None, + }); + } + let status = StatusCode::from_u16(status?).ok()?; if status.is_success() { return None; } - let body = error.map(|error| { - serde_json::to_string_pretty(&serde_json::json!({ - "error": error - })) - .unwrap_or_else(|_| { - serde_json::json!({ - "error": error - }) - .to_string() - }) - }); - Some(ApiError::Transport(TransportError::Http { status, url: None, headers: headers.map(json_headers_to_http_headers), - body, + body: Some(original_payload), })) } @@ -551,7 +563,8 @@ async fn run_websocket_response_stream( Message::Text(text) => { trace!("websocket event: {text}"); if let Some(wrapped_error) = parse_wrapped_websocket_error_event(&text) - && let Some(error) = map_wrapped_websocket_error_event(wrapped_error) + && let Some(error) = + map_wrapped_websocket_error_event(wrapped_error, text.to_string()) { return Err(error); } @@ -639,7 +652,7 @@ mod tests { let wrapped_error = parse_wrapped_websocket_error_event(&payload) .expect("expected websocket error payload to be parsed"); - let api_error = map_wrapped_websocket_error_event(wrapped_error) + let api_error = map_wrapped_websocket_error_event(wrapped_error, payload) .expect("expected websocket error payload to map to ApiError"); let ApiError::Transport(TransportError::Http { @@ -699,7 +712,7 @@ mod tests { let wrapped_error = parse_wrapped_websocket_error_event(&payload) .expect("expected websocket error payload to be parsed"); - let api_error = map_wrapped_websocket_error_event(wrapped_error) + let api_error = map_wrapped_websocket_error_event(wrapped_error, payload) .expect("expected websocket error payload to map to ApiError"); let ApiError::Transport(TransportError::Http { status, body, .. }) = api_error else { panic!("expected ApiError::Transport(Http)"); @@ -710,6 +723,30 @@ mod tests { assert!(body.contains("Model does not support image inputs")); } + #[test] + fn parse_wrapped_websocket_error_event_with_connection_limit_maps_retryable() { + let payload = json!({ + "type": "error", + "status": 400, + "error": { + "type": "invalid_request_error", + "code": "websocket_connection_limit_reached", + "message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue." + } + }) + .to_string(); + + let wrapped_error = parse_wrapped_websocket_error_event(&payload) + .expect("expected websocket error payload to be parsed"); + let api_error = map_wrapped_websocket_error_event(wrapped_error, payload) + .expect("expected websocket error payload to map to ApiError"); + let ApiError::Retryable { message, delay } = api_error else { + panic!("expected ApiError::Retryable"); + }; + assert_eq!(message, WEBSOCKET_CONNECTION_LIMIT_REACHED_MESSAGE); + assert_eq!(delay, None); + } + #[test] fn parse_wrapped_websocket_error_event_without_status_is_not_mapped() { let payload = json!({ @@ -727,7 +764,7 @@ mod tests { let wrapped_error = parse_wrapped_websocket_error_event(&payload) .expect("expected websocket error payload to be parsed"); - let api_error = map_wrapped_websocket_error_event(wrapped_error); + let api_error = map_wrapped_websocket_error_event(wrapped_error, payload); assert!(api_error.is_none()); } diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 9f7e380665..2b61f0de60 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -3,6 +3,7 @@ use codex_api::ModelsClient; use codex_api::provider::Provider; use codex_api::provider::RetryConfig; use codex_client::ReqwestTransport; +use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelVisibility; @@ -78,8 +79,10 @@ async fn models_client_hits_models_endpoint() { base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 5269148bc2..015fb87e37 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -14,6 +14,12 @@ codex_rust_crate( ) + [ "//codex-rs:node-version.txt", ], + rustc_env = { + # Askama resolves template paths relative to CARGO_MANIFEST_DIR. In + # Bazel, the Cargo-provided absolute source path points outside the + # sandbox, so keep the manifest root anchored inside the execroot. + "CARGO_MANIFEST_DIR": "codex-rs/core", + }, integration_compile_data_extra = [ "//codex-rs/apply-patch:apply_patch_tool_instructions.md", "models.json", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 820f35cb1a..87f18eb800 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -21,6 +21,7 @@ anyhow = { workspace = true } arc-swap = "1.8.2" async-channel = { workspace = true } async-trait = { workspace = true } +askama = { workspace = true } base64 = { workspace = true } bm25 = { workspace = true } chardetng = { workspace = true } @@ -50,6 +51,7 @@ codex-utils-pty = { workspace = true } codex-utils-readiness = { workspace = true } codex-secrets = { workspace = true } codex-utils-string = { workspace = true } +codex-utils-stream-parser = { workspace = true } codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" } csv = { workspace = true } dirs = { workspace = true } @@ -59,6 +61,7 @@ env-flags = { workspace = true } eventsource-stream = { workspace = true } futures = { workspace = true } http = { workspace = true } +iana-time-zone = { workspace = true } indexmap = { workspace = true } keyring = { workspace = true, features = ["crypto-rust"] } libc = { workspace = true } @@ -148,6 +151,7 @@ codex-arg0 = { workspace = true } codex-otel = { workspace = true, features = [ "disable-default-metrics-exporter", ] } +codex-test-macros = { workspace = true } codex-utils-cargo-bin = { workspace = true } core_test_support = { workspace = true } ctor = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ca7fb51579..59140d9d16 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -41,12 +41,6 @@ "minimum": 1.0, "type": "integer" }, - "max_spawn_depth": { - "description": "Maximum depth for thread-spawned subagents.", - "format": "uint", - "minimum": 1.0, - "type": "integer" - }, "max_threads": { "description": "Maximum number of agent threads that can be open concurrently. When unset, no limit is enforced.", "format": "uint", @@ -334,6 +328,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, @@ -412,9 +409,6 @@ "shell_zsh_fork": { "type": "boolean" }, - "skill_approval": { - "type": "boolean" - }, "skill_env_var_dependency_prompt": { "type": "boolean" }, @@ -619,6 +613,10 @@ "additionalProperties": false, "description": "Memories settings loaded from config.toml.", "properties": { + "generate_memories": { + "description": "When `false`, newly created threads are stored with `memory_mode = \"disabled\"` in the state DB.", + "type": "boolean" + }, "max_raw_memories_for_global": { "description": "Maximum number of recent raw memories retained for global consolidation.", "format": "uint", @@ -636,6 +634,11 @@ "minimum": 0.0, "type": "integer" }, + "max_unused_days": { + "description": "Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection.", + "format": "int64", + "type": "integer" + }, "min_rollout_idle_hours": { "description": "Minimum idle time between last thread activity and memory creation (hours). > 12h recommended.", "format": "int64", @@ -648,6 +651,10 @@ "phase_2_model": { "description": "Model used for memory consolidation.", "type": "string" + }, + "use_memories": { + "description": "When `false`, skip injecting memory usage instructions into developer prompts.", + "type": "boolean" } }, "type": "object" @@ -1147,6 +1154,10 @@ }, "type": "object" }, + "oauth_resource": { + "default": null, + "type": "string" + }, "required": { "default": null, "type": "boolean" @@ -1180,6 +1191,18 @@ }, "type": "object" }, + "RealtimeAudioToml": { + "additionalProperties": false, + "properties": { + "microphone": { + "type": "string" + }, + "speaker": { + "type": "string" + } + }, + "type": "object" + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -1541,6 +1564,15 @@ "default": null, "description": "Settings for app-specific controls." }, + "audio": { + "allOf": [ + { + "$ref": "#/definitions/RealtimeAudioToml" + } + ], + "default": null, + "description": "Machine-local realtime audio device preferences used by realtime voice." + }, "background_terminal_max_timeout": { "description": "Maximum poll window for background terminal output (`write_stdin`), in milliseconds. Default: `300000` (5 minutes).", "format": "uint64", @@ -1627,6 +1659,9 @@ "connectors": { "type": "boolean" }, + "default_mode_request_user_input": { + "type": "boolean" + }, "elevated_windows_sandbox": { "type": "boolean" }, @@ -1705,9 +1740,6 @@ "shell_zsh_fork": { "type": "boolean" }, - "skill_approval": { - "type": "boolean" - }, "skill_env_var_dependency_prompt": { "type": "boolean" }, @@ -2058,7 +2090,7 @@ "$ref": "#/definitions/AbsolutePathBuf" } ], - "description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses a temp dir under WorkspaceWrite sandboxing and `$CODEX_HOME` for other modes." + "description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`." }, "suppress_unstable_features_warning": { "description": "Suppress warnings about unstable (under development) features.", diff --git a/codex-rs/core/models.json b/codex-rs/core/models.json index 3f7d08b431..2fd55b9869 100644 --- a/codex-rs/core/models.json +++ b/codex-rs/core/models.json @@ -40,7 +40,7 @@ } ], "shell_type": "shell_command", - "visibility": "hide", + "visibility": "list", "minimal_client_version": "0.98.0", "supported_in_api": true, "upgrade": null, diff --git a/codex-rs/core/src/agent/agent_names.txt b/codex-rs/core/src/agent/agent_names.txt index e5a712ff5c..7574e69a80 100644 --- a/codex-rs/core/src/agent/agent_names.txt +++ b/codex-rs/core/src/agent/agent_names.txt @@ -1,87 +1,100 @@ -Ash -Elm -Yew -Fir -Oak -Pine -Spruce -Cedar -Birch -Maple -Beech -Alder -Willow -Poplar -Aspen -Larch -Juniper -Cypress -Hemlock -Sequoia -Baobab -Acacia -Banyan -Teak -Olive -Cherry -Apple -Pear -Plum -Peach -Mango -Guava -Fig -Date -Palm -Holly -Ivy -Fern -Moss -Reed -Cane -Rush -Sage -Mint -Thyme -Basil -Dill -Fennel -Parsley -Laurel -Myrtle -Lotus -Tulip -Rose -Daisy -Lily -Iris -Poppy -Aster -Peony -Camellia -Azalea -Begonia -Petunia -Verbena -Marigold -Daffodil -Orchid -Violet -Clover -Bamboo -Eucalypt -Oleander -Yucca -Agave -Aloe -Cactus -Sedum -Boxwood -Hawthorn -Redwood -Ironwood -Sandal -Kapok -Mahonia -Spirea -Wisteria \ No newline at end of file +Euclid +Archimedes +Ptolemy +Hypatia +Avicenna +Averroes +Aquinas +Copernicus +Kepler +Galileo +Bacon +Descartes +Pascal +Fermat +Huygens +Leibniz +Newton +Halley +Euler +Lagrange +Laplace +Volta +Gauss +Ampere +Faraday +Darwin +Lovelace +Boole +Pasteur +Maxwell +Mendel +Curie +Planck +Tesla +Poincare +Noether +Hilbert +Einstein +Raman +Bohr +Turing +Hubble +Feynman +Franklin +McClintock +Meitner +Herschel +Linnaeus +Wegener +Chandrasekhar +Sagan +Goodall +Carson +Carver +Socrates +Plato +Aristotle +Epicurus +Cicero +Confucius +Mencius +Zeno +Locke +Hume +Kant +Hegel +Kierkegaard +Mill +Nietzsche +Peirce +James +Dewey +Russell +Popper +Sartre +Beauvoir +Arendt +Rawls +Singer +Anscombe +Parfit +Kuhn +Boyle +Hooke +Harvey +Dalton +Ohm +Helmholtz +Gibbs +Lorentz +Schrodinger +Heisenberg +Pauli +Dirac +Bernoulli +Godel +Nash +Banach +Ramanujan +Erdos \ No newline at end of file diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 7287d25283..8ea2362283 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -4,11 +4,17 @@ use crate::agent::status::is_final; use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::find_thread_path_by_id_str; +use crate::rollout::RolloutRecorder; +use crate::session_prefix::format_subagent_context_line; use crate::session_prefix::format_subagent_notification_message; use crate::state_db; use crate::thread_manager::ThreadManagerState; use codex_protocol::ThreadId; +use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::Op; +use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; @@ -18,6 +24,12 @@ use std::sync::Weak; use tokio::sync::watch; const AGENT_NAMES: &str = include_str!("agent_names.txt"); +const FORKED_SPAWN_AGENT_OUTPUT_MESSAGE: &str = "You are the newly spawned agent. The prior conversation history was forked from your parent agent. Treat the next user message as your new task, and use the forked history only as background context."; + +#[derive(Clone, Debug, Default)] +pub(crate) struct SpawnAgentOptions { + pub(crate) fork_parent_spawn_call_id: Option, +} fn agent_nickname_list() -> Vec<&'static str> { AGENT_NAMES @@ -57,6 +69,17 @@ impl AgentControl { config: crate::config::Config, items: Vec, session_source: Option, + ) -> CodexResult { + self.spawn_agent_with_options(config, items, session_source, SpawnAgentOptions::default()) + .await + } + + pub(crate) async fn spawn_agent_with_options( + &self, + config: crate::config::Config, + items: Vec, + session_source: Option, + options: SpawnAgentOptions, ) -> CodexResult { let state = self.upgrade()?; let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; @@ -82,9 +105,75 @@ impl AgentControl { // The same `AgentControl` is sent to spawn the thread. let new_thread = match session_source { Some(session_source) => { - state - .spawn_new_thread_with_source(config, self.clone(), session_source, false) - .await? + if let Some(call_id) = options.fork_parent_spawn_call_id.as_ref() { + let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + .. + }) = session_source.clone() + else { + return Err(CodexErr::Fatal( + "spawn_agent fork requires a thread-spawn session source".to_string(), + )); + }; + let parent_thread = state.get_thread(parent_thread_id).await.ok(); + if let Some(parent_thread) = parent_thread.as_ref() { + // `record_conversation_items` only queues rollout writes asynchronously. + // Flush/materialize the live parent before snapshotting JSONL for a fork. + parent_thread + .codex + .session + .ensure_rollout_materialized() + .await; + parent_thread.codex.session.flush_rollout().await; + } + let rollout_path = parent_thread + .as_ref() + .and_then(|parent_thread| parent_thread.rollout_path()) + .or(find_thread_path_by_id_str( + config.codex_home.as_path(), + &parent_thread_id.to_string(), + ) + .await?) + .ok_or_else(|| { + CodexErr::Fatal(format!( + "parent thread rollout unavailable for fork: {parent_thread_id}" + )) + })?; + let mut forked_rollout_items = + RolloutRecorder::get_rollout_history(&rollout_path) + .await? + .get_rollout_items(); + let mut output = FunctionCallOutputPayload::from_text( + FORKED_SPAWN_AGENT_OUTPUT_MESSAGE.to_string(), + ); + output.success = Some(true); + forked_rollout_items.push(RolloutItem::ResponseItem( + ResponseItem::FunctionCallOutput { + call_id: call_id.clone(), + output, + }, + )); + let initial_history = InitialHistory::Forked(forked_rollout_items); + state + .fork_thread_with_source( + config, + initial_history, + self.clone(), + session_source, + false, + ) + .await? + } else { + state + .spawn_new_thread_with_source( + config, + self.clone(), + session_source, + false, + None, + ) + .await? + } } None => state.spawn_new_thread(config, self.clone()).await?, }; @@ -255,6 +344,40 @@ impl AgentControl { thread.total_token_usage().await } + pub(crate) async fn format_environment_context_subagents( + &self, + parent_thread_id: ThreadId, + ) -> String { + let Ok(state) = self.upgrade() else { + return String::new(); + }; + + let mut agents = Vec::new(); + for thread_id in state.list_thread_ids().await { + let Ok(thread) = state.get_thread(thread_id).await else { + continue; + }; + let snapshot = thread.config_snapshot().await; + let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id: agent_parent_thread_id, + agent_nickname, + .. + }) = snapshot.session_source + else { + continue; + }; + if agent_parent_thread_id != parent_thread_id { + continue; + } + agents.push(format_subagent_context_line( + &thread_id.to_string(), + agent_nickname.as_deref(), + )); + } + agents.sort(); + agents.join("\n") + } + /// Starts a detached watcher for sub-agents spawned from another thread. /// /// This is only enabled for `SubAgentSource::ThreadSpawn`, where a parent thread exists and @@ -319,8 +442,8 @@ mod tests { use crate::config::Config; use crate::config::ConfigBuilder; use crate::config_loader::LoaderOverrides; + use crate::contextual_user_message::SUBAGENT_NOTIFICATION_OPEN_TAG; use crate::features::Feature; - use crate::session_prefix::SUBAGENT_NOTIFICATION_OPEN_TAG; use assert_matches::assert_matches; use codex_protocol::config_types::ModeKind; use codex_protocol::models::ContentItem; @@ -421,6 +544,21 @@ mod tests { }) } + /// Returns true when any message item contains `needle` in a text span. + fn history_contains_text(history_items: &[ResponseItem], needle: &str) -> bool { + history_items.iter().any(|item| { + let ResponseItem::Message { content, .. } = item else { + return false; + }; + content.iter().any(|content_item| match content_item { + ContentItem::InputText { text } | ContentItem::OutputText { text } => { + text.contains(needle) + } + ContentItem::InputImage { .. } => false, + }) + }) + } + async fn wait_for_subagent_notification(parent_thread: &Arc) -> bool { let wait = async { loop { @@ -673,6 +811,242 @@ mod tests { assert_eq!(captured, Some(expected)); } + #[tokio::test] + async fn spawn_agent_can_fork_parent_thread_history() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, parent_thread) = harness.start_thread().await; + parent_thread + .inject_user_message_without_turn("parent seed context".to_string()) + .await; + let turn_context = parent_thread.codex.session.new_default_turn().await; + let parent_spawn_call_id = "spawn-call-history".to_string(); + let parent_spawn_call = ResponseItem::FunctionCall { + id: None, + name: "spawn_agent".to_string(), + arguments: "{}".to_string(), + call_id: parent_spawn_call_id.clone(), + }; + parent_thread + .codex + .session + .record_conversation_items(turn_context.as_ref(), &[parent_spawn_call]) + .await; + parent_thread + .codex + .session + .ensure_rollout_materialized() + .await; + parent_thread.codex.session.flush_rollout().await; + + let child_thread_id = harness + .control + .spawn_agent_with_options( + harness.config.clone(), + text_input("child task"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id), + }, + ) + .await + .expect("forked spawn should succeed"); + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + assert_ne!(child_thread_id, parent_thread_id); + let history = child_thread.codex.session.clone_history().await; + assert!(history_contains_text( + history.raw_items(), + "parent seed context" + )); + + let expected = ( + child_thread_id, + Op::UserInput { + items: vec![UserInput::Text { + text: "child task".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }, + ); + let captured = harness + .manager + .captured_ops() + .into_iter() + .find(|entry| *entry == expected); + assert_eq!(captured, Some(expected)); + + let _ = harness + .control + .shutdown_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + let _ = parent_thread + .submit(Op::Shutdown {}) + .await + .expect("parent shutdown should submit"); + } + + #[tokio::test] + async fn spawn_agent_fork_injects_output_for_parent_spawn_call() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, parent_thread) = harness.start_thread().await; + let turn_context = parent_thread.codex.session.new_default_turn().await; + let parent_spawn_call_id = "spawn-call-1".to_string(); + let parent_spawn_call = ResponseItem::FunctionCall { + id: None, + name: "spawn_agent".to_string(), + arguments: "{}".to_string(), + call_id: parent_spawn_call_id.clone(), + }; + parent_thread + .codex + .session + .record_conversation_items(turn_context.as_ref(), &[parent_spawn_call]) + .await; + parent_thread + .codex + .session + .ensure_rollout_materialized() + .await; + parent_thread.codex.session.flush_rollout().await; + + let child_thread_id = harness + .control + .spawn_agent_with_options( + harness.config.clone(), + text_input("child task"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), + }, + ) + .await + .expect("forked spawn should succeed"); + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + let history = child_thread.codex.session.clone_history().await; + let injected_output = history.raw_items().iter().find_map(|item| match item { + ResponseItem::FunctionCallOutput { call_id, output } + if call_id == &parent_spawn_call_id => + { + Some(output) + } + _ => None, + }); + let injected_output = + injected_output.expect("forked child should contain synthetic tool output"); + assert_eq!( + injected_output.text_content(), + Some(FORKED_SPAWN_AGENT_OUTPUT_MESSAGE) + ); + assert_eq!(injected_output.success, Some(true)); + + let _ = harness + .control + .shutdown_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + let _ = parent_thread + .submit(Op::Shutdown {}) + .await + .expect("parent shutdown should submit"); + } + + #[tokio::test] + async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, parent_thread) = harness.start_thread().await; + let turn_context = parent_thread.codex.session.new_default_turn().await; + let parent_spawn_call_id = "spawn-call-unflushed".to_string(); + let parent_spawn_call = ResponseItem::FunctionCall { + id: None, + name: "spawn_agent".to_string(), + arguments: "{}".to_string(), + call_id: parent_spawn_call_id.clone(), + }; + parent_thread + .codex + .session + .record_conversation_items(turn_context.as_ref(), &[parent_spawn_call]) + .await; + + let child_thread_id = harness + .control + .spawn_agent_with_options( + harness.config.clone(), + text_input("child task"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), + }, + ) + .await + .expect("forked spawn should flush parent rollout before loading history"); + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + let history = child_thread.codex.session.clone_history().await; + + let mut parent_call_index = None; + let mut injected_output_index = None; + for (idx, item) in history.raw_items().iter().enumerate() { + match item { + ResponseItem::FunctionCall { call_id, .. } if call_id == &parent_spawn_call_id => { + parent_call_index = Some(idx); + } + ResponseItem::FunctionCallOutput { call_id, .. } + if call_id == &parent_spawn_call_id => + { + injected_output_index = Some(idx); + } + _ => {} + } + } + + let parent_call_index = + parent_call_index.expect("forked child should include the parent spawn_agent call"); + let injected_output_index = injected_output_index + .expect("forked child should include synthetic output for the parent spawn_agent call"); + assert!(parent_call_index < injected_output_index); + + let _ = harness + .control + .shutdown_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + let _ = parent_thread + .submit(Op::Shutdown {}) + .await + .expect("parent shutdown should submit"); + } + #[tokio::test] async fn spawn_agent_respects_max_threads_limit() { let max_threads = 1usize; diff --git a/codex-rs/core/src/agent/guards.rs b/codex-rs/core/src/agent/guards.rs index b8db6e397d..4213cd0ed0 100644 --- a/codex-rs/core/src/agent/guards.rs +++ b/codex-rs/core/src/agent/guards.rs @@ -1,4 +1,3 @@ -use crate::config::DEFAULT_AGENT_MAX_SPAWN_DEPTH; use crate::error::CodexErr; use crate::error::Result; use codex_protocol::ThreadId; @@ -43,10 +42,6 @@ pub(crate) fn next_thread_spawn_depth(session_source: &SessionSource) -> i32 { session_depth(session_source).saturating_add(1) } -pub(crate) fn max_thread_spawn_depth(max_depth: Option) -> i32 { - let max_depth = max_depth.or(DEFAULT_AGENT_MAX_SPAWN_DEPTH).unwrap_or(1); - i32::try_from(max_depth).unwrap_or(i32::MAX) -} pub(crate) fn exceeds_thread_spawn_depth_limit(depth: i32, max_depth: i32) -> bool { depth > max_depth } diff --git a/codex-rs/core/src/agent/mod.rs b/codex-rs/core/src/agent/mod.rs index 6ae7d96159..15be909c3d 100644 --- a/codex-rs/core/src/agent/mod.rs +++ b/codex-rs/core/src/agent/mod.rs @@ -6,6 +6,5 @@ pub(crate) mod status; pub(crate) use codex_protocol::protocol::AgentStatus; pub(crate) use control::AgentControl; pub(crate) use guards::exceeds_thread_spawn_depth_limit; -pub(crate) use guards::max_thread_spawn_depth; pub(crate) use guards::next_thread_spawn_depth; pub(crate) use status::agent_status_from_event; diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index 8cc18c1ad1..3b34d3a71e 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -87,6 +87,7 @@ pub(crate) async fn apply_role_to_config( ConfigOverrides { cwd: Some(config.cwd.clone()), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), js_repl_node_path: config.js_repl_node_path.clone(), ..Default::default() }, @@ -186,19 +187,24 @@ Rules: config_file: None, } ), - ( - "awaiter".to_string(), - AgentRoleConfig { - description: Some(r#"Use an `awaiter` agent EVERY TIME you must run a command that might take some time. -This includes, but not only: -* testing -* monitoring of a long running process -* explicit ask to wait for something - -When YOU wait for the `awaiter` agent to be done, use the largest possible timeout."#.to_string()), - config_file: Some("awaiter.toml".to_string().parse().unwrap_or_default()), - } - ) + // Awaiter is temp removed +// ( +// "awaiter".to_string(), +// AgentRoleConfig { +// description: Some(r#"Use an `awaiter` agent EVERY TIME you must run a command that will take some very long time. +// This includes, but not only: +// * testing +// * monitoring of a long running process +// * explicit ask to wait for something +// +// Rules: +// - When an awaiter is running, you can work on something else. If you need to wait for its completion, use the largest possible timeout. +// - Be patient with the `awaiter`. +// - Do not use an awaiter for every compilation/test if it won't take time. Only use if for long running commands. +// - Close the awaiter when you're done with it."#.to_string()), +// config_file: Some("awaiter.toml".to_string().parse().unwrap_or_default()), +// } +// ) ]) }); &CONFIG @@ -340,6 +346,8 @@ mod tests { TomlValue::String("base-model".to_string()), )]) .await; + config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox")); + config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper")); let role_path = write_role_config( &home, "effort-only.toml", @@ -360,6 +368,14 @@ mod tests { assert_eq!(config.model.as_deref(), Some("base-model")); assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!( + config.codex_linux_sandbox_exe, + Some(PathBuf::from("/tmp/codex-linux-sandbox")) + ); + assert_eq!( + config.main_execve_wrapper_exe, + Some(PathBuf::from("/tmp/codex-execve-wrapper")) + ); } #[tokio::test] diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 74a2f15c19..26fbf5e07b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -315,7 +315,10 @@ impl ModelClient { instructions: &instructions, }; - let extra_headers = self.build_subagent_headers(); + let mut extra_headers = self.build_subagent_headers(); + extra_headers.extend(build_conversation_headers(Some( + self.state.conversation_id.to_string(), + ))); client .compact_input(&payload, extra_headers) .await @@ -392,8 +395,8 @@ impl ModelClient { /// This combines provider capability and feature gating; both must be true for websocket paths /// to be eligible. /// - /// If websockets are only enabled via model preference (no explicit feature flag), default to - /// v1 behavior. + /// If websockets are only enabled via model preference (no explicit feature flag), prefer the + /// current v2 behavior. pub fn active_ws_version(&self, model_info: &ModelInfo) -> Option { if !self.state.provider.supports_websockets || self.state.disable_websockets.load(Ordering::Relaxed) @@ -403,7 +406,7 @@ impl ModelClient { match self.state.responses_websocket_version { Some(version) => Some(version), - None if model_info.prefer_websockets => Some(ResponsesWebsocketVersion::V1), + None if model_info.prefer_websockets => Some(ResponsesWebsocketVersion::V2), None => None, } } diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 33fc438727..3fea6eed56 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -84,19 +84,13 @@ fn reserialize_shell_outputs(items: &mut [ResponseItem]) { shell_call_ids.insert(call_id.clone()); } } - ResponseItem::CustomToolCallOutput { call_id, output } => { - if shell_call_ids.remove(call_id) - && let Some(structured) = parse_structured_shell_output(output) - { - *output = structured - } - } ResponseItem::FunctionCall { name, call_id, .. } if is_shell_tool_name(name) || name == "apply_patch" => { shell_call_ids.insert(call_id.clone()); } - ResponseItem::FunctionCallOutput { call_id, output } => { + ResponseItem::FunctionCallOutput { call_id, output } + | ResponseItem::CustomToolCallOutput { call_id, output } => { if shell_call_ids.remove(call_id) && let Some(structured) = output .text_content() @@ -240,6 +234,7 @@ mod tests { use codex_api::common::OpenAiVerbosity; use codex_api::common::TextControls; use codex_api::create_text_param_for_request; + use codex_protocol::models::FunctionCallOutputPayload; use pretty_assertions::assert_eq; use super::*; @@ -343,4 +338,62 @@ mod tests { let v = serde_json::to_value(&req).expect("json"); assert!(v.get("text").is_none()); } + + #[test] + fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { + let raw_output = r#"{"output":"hello","metadata":{"exit_code":0,"duration_seconds":0.5}}"#; + let expected_output = "Exit code: 0\nWall time: 0.5 seconds\nOutput:\nhello"; + let mut items = vec![ + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + arguments: "{}".to_string(), + call_id: "call-1".to_string(), + }, + ResponseItem::FunctionCallOutput { + call_id: "call-1".to_string(), + output: FunctionCallOutputPayload::from_text(raw_output.to_string()), + }, + ResponseItem::CustomToolCall { + id: None, + status: None, + call_id: "call-2".to_string(), + name: "apply_patch".to_string(), + input: "*** Begin Patch".to_string(), + }, + ResponseItem::CustomToolCallOutput { + call_id: "call-2".to_string(), + output: FunctionCallOutputPayload::from_text(raw_output.to_string()), + }, + ]; + + reserialize_shell_outputs(&mut items); + + assert_eq!( + items, + vec![ + ResponseItem::FunctionCall { + id: None, + name: "shell".to_string(), + arguments: "{}".to_string(), + call_id: "call-1".to_string(), + }, + ResponseItem::FunctionCallOutput { + call_id: "call-1".to_string(), + output: FunctionCallOutputPayload::from_text(expected_output.to_string()), + }, + ResponseItem::CustomToolCall { + id: None, + status: None, + call_id: "call-2".to_string(), + name: "apply_patch".to_string(), + input: "*** Begin Patch".to_string(), + }, + ResponseItem::CustomToolCallOutput { + call_id: "call-2".to_string(), + output: FunctionCallOutputPayload::from_text(expected_output.to_string()), + }, + ] + ); + } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d3d789c4ed..91b24a0f25 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -29,6 +29,8 @@ use crate::features::FEATURES; use crate::features::Feature; use crate::features::Features; use crate::features::maybe_push_unstable_features_warning; +#[cfg(test)] +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; use crate::parse_command::parse_command; use crate::parse_turn_item; @@ -42,6 +44,8 @@ use crate::stream_events_utils::HandleOutputCtx; use crate::stream_events_utils::handle_non_tool_response_item; use crate::stream_events_utils::handle_output_item_done; use crate::stream_events_utils::last_assistant_message_from_item; +use crate::stream_events_utils::raw_assistant_output_text_from_item; +use crate::stream_events_utils::record_completed_response_item; use crate::terminal; use crate::truncate::TruncationPolicy; use crate::turn_metadata::TurnMetadataState; @@ -49,6 +53,8 @@ use crate::util::error_or_panic; use crate::ws_version_from_features; use async_channel::Receiver; use async_channel::Sender; +use chrono::Local; +use chrono::Utc; use codex_hooks::HookEvent; use codex_hooks::HookEventAfterAgent; use codex_hooks::HookPayload; @@ -56,6 +62,7 @@ use codex_hooks::HookResult; use codex_hooks::Hooks; use codex_hooks::HooksConfig; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyAuditMetadata; use codex_network_proxy::normalize_host; use codex_protocol::ThreadId; use codex_protocol::approvals::ExecPolicyAmendment; @@ -70,8 +77,8 @@ use codex_protocol::items::PlanItem; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::mcp::CallToolResult; -use codex_protocol::models::AdditionalPermissions; use codex_protocol::models::BaseInstructions; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::FileChange; @@ -89,9 +96,13 @@ use codex_protocol::protocol::TurnContextNetworkItem; use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputResponse; -use codex_protocol::skill_approval::SkillApprovalResponse; use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::OAuthCredentialsStoreMode; +use codex_utils_stream_parser::AssistantTextChunk; +use codex_utils_stream_parser::AssistantTextStreamParser; +use codex_utils_stream_parser::ProposedPlanSegment; +use codex_utils_stream_parser::extract_proposed_plan_text; +use codex_utils_stream_parser::strip_citations; use futures::future::BoxFuture; use futures::prelude::*; use futures::stream::FuturesOrdered; @@ -174,9 +185,6 @@ use crate::mentions::collect_explicit_app_ids; use crate::mentions::collect_tool_mentions_from_messages; use crate::network_policy_decision::execpolicy_network_rule_amendment; use crate::project_doc::get_user_instructions; -use crate::proposed_plan_parser::ProposedPlanParser; -use crate::proposed_plan_parser::ProposedPlanSegment; -use crate::proposed_plan_parser::extract_proposed_plan_text; use crate::protocol::AgentMessageContentDeltaEvent; use crate::protocol::AgentReasoningSectionBreakEvent; use crate::protocol::ApplyPatchApprovalRequestEvent; @@ -192,7 +200,6 @@ use crate::protocol::ModelRerouteEvent; use crate::protocol::ModelRerouteReason; use crate::protocol::NetworkApprovalContext; use crate::protocol::Op; -use crate::protocol::PatchApplyStatus; use crate::protocol::PlanDeltaEvent; use crate::protocol::RateLimitSnapshot; use crate::protocol::ReasoningContentDeltaEvent; @@ -246,6 +253,7 @@ use crate::tools::ToolRouter; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME; use crate::tools::js_repl::JsReplHandle; +use crate::tools::js_repl::resolve_compatible_node; use crate::tools::network_approval::NetworkApprovalService; use crate::tools::network_approval::build_blocked_request_observer; use crate::tools::network_approval::build_network_policy_decider; @@ -315,6 +323,7 @@ impl Codex { agent_control: AgentControl, dynamic_tools: Vec, persist_extended_history: bool, + metrics_service_name: Option, ) -> CodexResult { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); @@ -335,6 +344,18 @@ impl Codex { config.features.disable(Feature::Collab); } + if config.features.enabled(Feature::JsRepl) + && let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await + { + let message = format!( + "Disabled `js_repl` for this session because the configured Node runtime is unavailable or incompatible. {err}" + ); + warn!("{message}"); + config.features.disable(Feature::JsRepl); + config.features.disable(Feature::JsReplToolsOnly); + config.startup_warnings.push(message); + } + let allowed_skills_for_implicit_invocation = loaded_skills.allowed_skills_for_implicit_invocation(); let user_instructions = @@ -421,6 +442,8 @@ impl Codex { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name, + app_server_client_name: None, session_source, dynamic_tools, persist_extended_history, @@ -508,6 +531,18 @@ impl Codex { self.session.steer_input(input, expected_turn_id).await } + pub(crate) async fn set_app_server_client_name( + &self, + app_server_client_name: Option, + ) -> ConstraintResult<()> { + self.session + .update_settings(SessionSettingsUpdate { + app_server_client_name, + ..Default::default() + }) + .await + } + pub(crate) async fn agent_status(&self) -> AgentStatus { self.agent_status.borrow().clone() } @@ -575,6 +610,9 @@ pub(crate) struct TurnContext { /// the model as well as sandbox policies are resolved against this path /// instead of `std::env::current_dir()`. pub(crate) cwd: PathBuf, + pub(crate) current_date: Option, + pub(crate) timezone: Option, + pub(crate) app_server_client_name: Option, pub(crate) developer_instructions: Option, pub(crate) compact_prompt: Option, pub(crate) user_instructions: Option, @@ -659,6 +697,9 @@ impl TurnContext { reasoning_summary: self.reasoning_summary, session_source: self.session_source.clone(), cwd: self.cwd.clone(), + current_date: self.current_date.clone(), + timezone: self.timezone.clone(), + app_server_client_name: self.app_server_client_name.clone(), developer_instructions: self.developer_instructions.clone(), compact_prompt: self.compact_prompt.clone(), user_instructions: self.user_instructions.clone(), @@ -699,6 +740,8 @@ impl TurnContext { TurnContextItem { turn_id: Some(self.sub_id.clone()), cwd: self.cwd.clone(), + current_date: self.current_date.clone(), + timezone: self.timezone.clone(), approval_policy: self.approval_policy.value(), sandbox_policy: self.sandbox_policy.get().clone(), network: self.turn_context_network_item(), @@ -728,13 +771,23 @@ impl TurnContext { } } +fn local_time_context() -> (String, String) { + match iana_time_zone::get_timezone() { + Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), + Err(_) => ( + Utc::now().format("%Y-%m-%d").to_string(), + "Etc/UTC".to_string(), + ), + } +} + #[derive(Clone)] pub(crate) struct SessionConfiguration { /// Provider identifier ("openai", "openrouter", ...). provider: ModelProviderInfo, collaboration_mode: CollaborationMode, - model_reasoning_summary: ReasoningSummaryConfig, + model_reasoning_summary: Option, /// Developer instructions that supplement the base instructions. developer_instructions: Option, @@ -772,6 +825,9 @@ pub(crate) struct SessionConfiguration { // TODO(pakrym): Remove config from here original_config_do_not_use: Arc, + /// Optional service name tag for session metrics. + metrics_service_name: Option, + app_server_client_name: Option, /// Source of the session (cli, vscode, exec, mcp, ...) session_source: SessionSource, dynamic_tools: Vec, @@ -802,7 +858,7 @@ impl SessionConfiguration { next_configuration.collaboration_mode = collaboration_mode; } if let Some(summary) = updates.reasoning_summary { - next_configuration.model_reasoning_summary = summary; + next_configuration.model_reasoning_summary = Some(summary); } if let Some(personality) = updates.personality { next_configuration.personality = Some(personality); @@ -819,6 +875,9 @@ impl SessionConfiguration { if let Some(cwd) = updates.cwd.clone() { next_configuration.cwd = cwd; } + if let Some(app_server_client_name) = updates.app_server_client_name.clone() { + next_configuration.app_server_client_name = Some(app_server_client_name); + } Ok(next_configuration) } } @@ -833,6 +892,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) reasoning_summary: Option, pub(crate) final_output_json_schema: Option>, pub(crate) personality: Option, + pub(crate) app_server_client_name: Option, } impl Session { @@ -869,6 +929,7 @@ impl Session { network_policy_decider: Option>, blocked_request_observer: Option>, managed_network_requirements_enabled: bool, + audit_metadata: NetworkProxyAuditMetadata, ) -> anyhow::Result<(StartedNetworkProxy, SessionNetworkProxyRuntime)> { let network_proxy = spec .start_proxy( @@ -876,6 +937,7 @@ impl Session { network_policy_decider, blocked_request_observer, managed_network_requirements_enabled, + audit_metadata, ) .await .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?; @@ -961,7 +1023,9 @@ impl Session { skills_outcome: Arc, ) -> TurnContext { let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); - let reasoning_summary = session_configuration.model_reasoning_summary; + let reasoning_summary = session_configuration + .model_reasoning_summary + .unwrap_or(model_info.default_reasoning_summary); let otel_manager = otel_manager.clone().with_model( session_configuration.collaboration_mode.model(), model_info.slug.as_str(), @@ -991,6 +1055,7 @@ impl Session { .features .enabled(Feature::UseLinuxSandboxBwrap), )); + let (current_date, timezone) = local_time_context(); TurnContext { sub_id, config: per_turn_config.clone(), @@ -1002,6 +1067,9 @@ impl Session { reasoning_summary, session_source, cwd, + current_date: Some(current_date), + timezone: Some(timezone), + app_server_client_name: session_configuration.app_server_client_name.clone(), developer_instructions: session_configuration.developer_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), @@ -1100,7 +1168,7 @@ impl Session { // // - initialize RolloutRecorder with new or resumed session info // - perform default shell discovery - // - load history metadata + // - load history metadata (skipped for subagents) let rollout_fut = async { if config.ephemeral { Ok::<_, anyhow::Error>((None, None)) @@ -1117,7 +1185,16 @@ impl Session { } }; - let history_meta_fut = crate::message_history::history_metadata(&config); + let history_meta_fut = async { + if matches!( + session_configuration.session_source, + SessionSource::SubAgent(_) + ) { + (0, 0) + } else { + crate::message_history::history_metadata(&config).await + } + }; let auth_manager_clone = Arc::clone(&auth_manager); let config_for_mcp = Arc::clone(&config); let auth_and_mcp_fut = async move { @@ -1190,18 +1267,37 @@ impl Session { let auth = auth.as_ref(); let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from); - let otel_manager = OtelManager::new( + let account_id = auth.and_then(CodexAuth::get_account_id); + let account_email = auth.and_then(CodexAuth::get_account_email); + let originator = crate::default_client::originator().value; + let terminal_type = terminal::user_agent(); + let session_model = session_configuration.collaboration_mode.model().to_string(); + let mut otel_manager = OtelManager::new( conversation_id, - session_configuration.collaboration_mode.model(), - session_configuration.collaboration_mode.model(), - auth.and_then(CodexAuth::get_account_id), - auth.and_then(CodexAuth::get_account_email), + session_model.as_str(), + session_model.as_str(), + account_id.clone(), + account_email.clone(), auth_mode, - crate::default_client::originator().value, + originator.clone(), config.otel.log_user_prompt, - terminal::user_agent(), + terminal_type.clone(), session_configuration.session_source.clone(), ); + if let Some(service_name) = session_configuration.metrics_service_name.as_deref() { + otel_manager = otel_manager.with_metrics_service_name(service_name); + } + let network_proxy_audit_metadata = NetworkProxyAuditMetadata { + conversation_id: Some(conversation_id.to_string()), + app_version: Some(env!("CARGO_PKG_VERSION").to_string()), + user_account_id: account_id, + auth_mode: auth_mode.map(|mode| mode.to_string()), + originator: Some(originator), + user_email: account_email, + terminal_type: Some(terminal_type), + model: Some(session_model.clone()), + slug: Some(session_model), + }; config.features.emit_metrics(&otel_manager); otel_manager.counter( "codex.thread.started", @@ -1219,7 +1315,9 @@ impl Session { otel_manager.conversation_starts( config.model_provider.name.as_str(), session_configuration.collaboration_mode.reasoning_effort(), - config.model_reasoning_summary, + config + .model_reasoning_summary + .unwrap_or(ReasoningSummaryConfig::Auto), config.model_context_window, config.model_auto_compact_token_limit, config.permissions.approval_policy.value(), @@ -1308,6 +1406,7 @@ impl Session { network_policy_decider.as_ref().map(Arc::clone), blocked_request_observer.as_ref().map(Arc::clone), managed_network_requirements_enabled, + network_proxy_audit_metadata, ) .await?; (Some(network_proxy), Some(session_network_proxy)) @@ -1331,6 +1430,7 @@ impl Session { config.background_terminal_max_timeout, ), shell_zsh_path: config.zsh_path.clone(), + main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( Arc::clone(&config), Arc::clone(&auth_manager), @@ -1347,6 +1447,7 @@ impl Session { otel_manager, models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, @@ -1623,7 +1724,10 @@ impl Session { match conversation_history { InitialHistory::New => { // Build and record initial items (user instructions + environment context) - let items = self.build_initial_context(&turn_context).await; + // TODO(ccunningham): Defer initial context insertion until the first real turn + // starts so it reflects the actual first-turn settings (permissions, etc.) and + // we do not emit model-visible "diff" updates before the first user message. + let items = self.build_initial_context(&turn_context, None).await; self.record_conversation_items(&turn_context, &items).await; { let mut state = self.state.lock().await; @@ -1727,7 +1831,7 @@ impl Session { } // Append the current session's initial context after the reconstructed history. - let initial_context = self.build_initial_context(&turn_context).await; + let initial_context = self.build_initial_context(&turn_context, None).await; self.record_conversation_items(&turn_context, &initial_context) .await; { @@ -2556,9 +2660,13 @@ impl Session { /// Emit an exec approval request event and await the user's decision. /// - /// The request is keyed by `call_id` + `approval_id` so matching responses are delivered - /// to the correct in-flight turn. If the task is aborted, this returns the - /// default `ReviewDecision` (`Denied`). + /// The request is keyed by `call_id` + `approval_id` so matching responses + /// are delivered to the correct in-flight turn. If the task is aborted, + /// this returns the default `ReviewDecision` (`Denied`). + /// + /// Note that if `available_decisions` is `None`, then the other fields will + /// be used to derive the available decisions via + /// [ExecApprovalRequestEvent::default_available_decisions]. #[allow(clippy::too_many_arguments)] pub async fn request_command_approval( &self, @@ -2570,7 +2678,8 @@ impl Session { reason: Option, network_approval_context: Option, proposed_execpolicy_amendment: Option, - additional_permissions: Option, + additional_permissions: Option, + available_decisions: Option>, ) -> ReviewDecision { // command-level approvals use `call_id`. // `approval_id` is only present for subcommand callbacks (execve intercept) @@ -2604,6 +2713,14 @@ impl Session { }, ] }); + let available_decisions = available_decisions.unwrap_or_else(|| { + ExecApprovalRequestEvent::default_available_decisions( + network_approval_context.as_ref(), + proposed_execpolicy_amendment.as_ref(), + proposed_network_policy_amendments.as_deref(), + additional_permissions.as_ref(), + ) + }); let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id, approval_id, @@ -2615,6 +2732,7 @@ impl Session { proposed_execpolicy_amendment, proposed_network_policy_amendments, additional_permissions, + available_decisions: Some(available_decisions), parsed_cmd, }); self.send_event(turn_context, event).await; @@ -2689,40 +2807,6 @@ impl Session { rx_response.await.ok() } - pub async fn request_skill_approval( - &self, - turn_context: &TurnContext, - item_id: String, - skill_name: String, - ) -> Option { - let (tx_response, rx_response) = oneshot::channel(); - let prev_entry = { - let mut active = self.active_turn.lock().await; - match active.as_mut() { - Some(at) => { - let mut ts = at.turn_state.lock().await; - ts.insert_pending_skill_approval(item_id.clone(), tx_response) - } - None => None, - } - }; - if prev_entry.is_some() { - warn!("Overwriting existing pending skill approval for item_id: {item_id}"); - } - - self.send_event( - turn_context, - EventMsg::SkillRequestApproval( - codex_protocol::skill_approval::SkillRequestApprovalEvent { - item_id, - skill_name, - }, - ), - ) - .await; - rx_response.await.ok() - } - pub async fn notify_user_input_response( &self, sub_id: &str, @@ -2769,31 +2853,6 @@ impl Session { } } - pub async fn notify_skill_approval_response( - &self, - item_id: &str, - response: SkillApprovalResponse, - ) { - let entry = { - let mut active = self.active_turn.lock().await; - match active.as_mut() { - Some(at) => { - let mut ts = at.turn_state.lock().await; - ts.remove_pending_skill_approval(item_id) - } - None => None, - } - }; - match entry { - Some(tx_response) => { - tx_response.send(response).ok(); - } - None => { - warn!("No pending skill approval found for item_id: {item_id}"); - } - } - } - pub async fn notify_approval(&self, approval_id: &str, decision: ReviewDecision) { let entry = { let mut active = self.active_turn.lock().await; @@ -2861,7 +2920,7 @@ impl Session { } else { let user_messages = collect_user_messages(history.raw_items()); let rebuilt = compact::build_compacted_history( - self.build_initial_context(turn_context).await, + self.build_initial_context(turn_context, None).await, &user_messages, &compacted.message, ); @@ -2989,10 +3048,20 @@ impl Session { pub(crate) async fn build_initial_context( &self, turn_context: &TurnContext, + previous_user_turn_model: Option<&str>, ) -> Vec { - let mut items = Vec::::with_capacity(4); + let mut developer_sections = Vec::::with_capacity(8); + let mut contextual_user_sections = Vec::::with_capacity(2); let shell = self.user_shell(); - items.push( + if let Some(model_switch_message) = + crate::context_manager::updates::build_model_instructions_update_item( + previous_user_turn_model, + turn_context, + ) + { + developer_sections.push(model_switch_message.into_text()); + } + developer_sections.push( DeveloperInstructions::from_policy( turn_context.sandbox_policy.get(), turn_context.approval_policy.value(), @@ -3000,17 +3069,18 @@ impl Session { &turn_context.cwd, turn_context.features.enabled(Feature::RequestPermissions), ) - .into(), + .into_text(), ); if let Some(developer_instructions) = turn_context.developer_instructions.as_deref() { - items.push(DeveloperInstructions::new(developer_instructions.to_string()).into()); + developer_sections.push(developer_instructions.to_string()); } // Add developer instructions for memories. - if let Some(memory_prompt) = - build_memory_tool_developer_instructions(&turn_context.config.codex_home).await - && turn_context.features.enabled(Feature::MemoryTool) + if turn_context.features.enabled(Feature::MemoryTool) + && turn_context.config.memories.use_memories + && let Some(memory_prompt) = + build_memory_tool_developer_instructions(&turn_context.config.codex_home).await { - items.push(DeveloperInstructions::new(memory_prompt).into()); + developer_sections.push(memory_prompt); } // Add developer instructions from collaboration_mode if they exist and are non-empty let (collaboration_mode, base_instructions) = { @@ -3023,7 +3093,7 @@ impl Session { if let Some(collab_instructions) = DeveloperInstructions::from_collaboration_mode(&collaboration_mode) { - items.push(collab_instructions.into()); + developer_sections.push(collab_instructions.into_text()); } if self.features.enabled(Feature::Personality) && let Some(personality) = turn_context.personality @@ -3038,34 +3108,53 @@ impl Session { personality, ) { - items.push( - DeveloperInstructions::personality_spec_message(personality_message).into(), + developer_sections.push( + DeveloperInstructions::personality_spec_message(personality_message) + .into_text(), ); } } if turn_context.features.enabled(Feature::Apps) { - items.push(DeveloperInstructions::new(render_apps_section()).into()); + developer_sections.push(render_apps_section()); } if turn_context.features.enabled(Feature::CodexGitCommit) && let Some(commit_message_instruction) = commit_message_trailer_instruction( turn_context.config.commit_attribution.as_deref(), ) { - items.push(DeveloperInstructions::new(commit_message_instruction).into()); + developer_sections.push(commit_message_instruction); } if let Some(user_instructions) = turn_context.user_instructions.as_deref() { - items.push( + contextual_user_sections.push( UserInstructions { text: user_instructions.to_string(), directory: turn_context.cwd.to_string_lossy().into_owned(), } - .into(), + .serialize_to_text(), ); } - items.push(ResponseItem::from(EnvironmentContext::from_turn_context( - turn_context, - shell.as_ref(), - ))); + let subagents = self + .services + .agent_control + .format_environment_context_subagents(self.conversation_id) + .await; + contextual_user_sections.push( + EnvironmentContext::from_turn_context(turn_context, shell.as_ref()) + .with_subagents(subagents) + .serialize_to_xml(), + ); + + let mut items = Vec::with_capacity(2); + if let Some(developer_message) = + crate::context_manager::updates::build_developer_update_item(developer_sections) + { + items.push(developer_message); + } + if let Some(contextual_user_message) = + crate::context_manager::updates::build_contextual_user_message(contextual_user_sections) + { + items.push(contextual_user_message); + } items } @@ -3110,22 +3199,8 @@ impl Session { let reference_context_item = self.reference_context_item().await; let should_inject_full_context = reference_context_item.is_none(); let context_items = if should_inject_full_context { - let mut initial_context = self.build_initial_context(turn_context).await; - // Full reinjection bypasses the settings-diff path, so add the model-switch - // instruction explicitly when needed. Keep it before the rest of full context so - // model-specific guidance is read first. - if let Some(model_switch_item) = - crate::context_manager::updates::build_model_instructions_update_item( - previous_user_turn_model, - turn_context, - ) - { - // TODO(ccunningham): When a model switch changes the effective personality - // instructions, inject the updated personality spec alongside - // here so resume/model-switch paths can avoid forcing full reinjection. - initial_context.insert(0, model_switch_item); - } - initial_context + self.build_initial_context(turn_context, previous_user_turn_model) + .await } else { // Steady-state path: append only context diffs to minimize token overhead. self.build_settings_update_items( @@ -3515,9 +3590,9 @@ impl Session { use_linux_sandbox_bwrap: turn_context.features.enabled(Feature::UseLinuxSandboxBwrap), }; { - let mut cancel_guard = self.services.mcp_startup_cancellation_token.lock().await; - cancel_guard.cancel(); - *cancel_guard = CancellationToken::new(); + let mut guard = self.services.mcp_startup_cancellation_token.lock().await; + guard.cancel(); + *guard = CancellationToken::new(); } let (refreshed_manager, cancel_token) = McpConnectionManager::new( &mcp_servers, @@ -3531,11 +3606,11 @@ impl Session { ) .await; { - let mut cancel_guard = self.services.mcp_startup_cancellation_token.lock().await; - if cancel_guard.is_cancelled() { + let mut guard = self.services.mcp_startup_cancellation_token.lock().await; + if guard.is_cancelled() { cancel_token.cancel(); } - *cancel_guard = cancel_token; + *guard = cancel_token; } let mut manager = self.services.mcp_connection_manager.write().await; @@ -3697,9 +3772,6 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::DynamicToolResponse { id, response } => { handlers::dynamic_tool_response(&sess, id, response).await; } - Op::SkillApproval { id, response } => { - handlers::skill_approval_response(&sess, id, response).await; - } Op::AddToHistory { text } => { handlers::add_to_history(&sess, &config, text).await; } @@ -3823,7 +3895,6 @@ mod handlers { use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::WarningEvent; use codex_protocol::request_user_input::RequestUserInputResponse; - use codex_protocol::skill_approval::SkillApprovalResponse; use crate::context_manager::is_user_turn_boundary; use codex_protocol::config_types::CollaborationMode; @@ -3896,9 +3967,10 @@ mod handlers { sandbox_policy: Some(sandbox_policy), windows_sandbox_level: None, collaboration_mode, - reasoning_summary: Some(summary), + reasoning_summary: summary, final_output_json_schema: Some(final_output_json_schema), personality, + app_server_client_name: None, }, ) } @@ -4064,14 +4136,6 @@ mod handlers { sess.notify_dynamic_tool_response(&id, response).await; } - pub async fn skill_approval_response( - sess: &Arc, - id: String, - response: SkillApprovalResponse, - ) { - sess.notify_skill_approval_response(&id, response).await; - } - pub async fn add_to_history(sess: &Arc, config: &Arc, text: String) { let id = sess.conversation_id; let config = Arc::clone(config); @@ -4626,7 +4690,9 @@ async fn spawn_review_thread( let provider_for_context = provider.clone(); let otel_manager_for_context = otel_manager.clone(); let reasoning_effort = per_turn_config.model_reasoning_effort; - let reasoning_summary = per_turn_config.model_reasoning_summary; + let reasoning_summary = per_turn_config + .model_reasoning_summary + .unwrap_or(model_info.default_reasoning_summary); let session_source = parent_turn_context.session_source.clone(); let per_turn_config = Arc::new(per_turn_config); @@ -4654,6 +4720,9 @@ async fn spawn_review_thread( tools_config, features: parent_turn_context.features.clone(), ghost_snapshot: parent_turn_context.ghost_snapshot.clone(), + current_date: parent_turn_context.current_date.clone(), + timezone: parent_turn_context.timezone.clone(), + app_server_client_name: parent_turn_context.app_server_client_name.clone(), developer_instructions: None, user_instructions: None, compact_prompt: parent_turn_context.compact_prompt.clone(), @@ -5022,6 +5091,7 @@ pub(crate) async fn run_turn( &sess, &turn_context, InitialContextInjection::BeforeLastUserMessage, + previous_model.as_deref(), ) .await .is_err() @@ -5038,6 +5108,7 @@ pub(crate) async fn run_turn( .dispatch(HookPayload { session_id: sess.conversation_id, cwd: turn_context.cwd.clone(), + client: turn_context.app_server_client_name.clone(), triggered_at: chrono::Utc::now(), hook_event: HookEvent::AfterAgent { event: HookEventAfterAgent { @@ -5145,7 +5216,13 @@ async fn run_pre_sampling_compact( .unwrap_or(i64::MAX); // Compact if the total usage tokens are greater than the auto compact limit if total_usage_tokens >= auto_compact_limit { - run_auto_compact(sess, turn_context, InitialContextInjection::DoNotInject).await?; + run_auto_compact( + sess, + turn_context, + InitialContextInjection::DoNotInject, + None, + ) + .await?; } Ok(()) } @@ -5188,6 +5265,7 @@ async fn maybe_run_previous_model_inline_compact( sess, &previous_model_turn_context, InitialContextInjection::DoNotInject, + None, ) .await?; return Ok(true); @@ -5199,12 +5277,14 @@ async fn run_auto_compact( sess: &Arc, turn_context: &Arc, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { if should_use_remote_compact_task(&turn_context.provider) { run_inline_remote_auto_compact_task( Arc::clone(sess), Arc::clone(turn_context), initial_context_injection, + previous_user_turn_model, ) .await?; } else { @@ -5212,6 +5292,7 @@ async fn run_auto_compact( Arc::clone(sess), Arc::clone(turn_context), initial_context_injection, + previous_user_turn_model, ) .await?; } @@ -5609,39 +5690,9 @@ struct ProposedPlanItemState { completed: bool, } -/// Per-item plan parsers so we can buffer text while detecting `` -/// tags without ever mixing buffered lines across item ids. -struct PlanParsers { - assistant: HashMap, -} - -impl PlanParsers { - fn new() -> Self { - Self { - assistant: HashMap::new(), - } - } - - fn assistant_parser_mut(&mut self, item_id: &str) -> &mut ProposedPlanParser { - self.assistant - .entry(item_id.to_string()) - .or_insert_with(ProposedPlanParser::new) - } - - fn take_assistant_parser(&mut self, item_id: &str) -> Option { - self.assistant.remove(item_id) - } - - fn drain_assistant_parsers(&mut self) -> Vec<(String, ProposedPlanParser)> { - self.assistant.drain().collect() - } -} - /// Aggregated state used only while streaming a plan-mode response. /// Includes per-item parsers, deferred agent message bookkeeping, and the plan item lifecycle. struct PlanModeStreamState { - /// Per-item parsers for assistant streams in plan mode. - plan_parsers: PlanParsers, /// Agent message items started by the model but deferred until we see non-plan text. pending_agent_message_items: HashMap, /// Agent message items whose start notification has been emitted. @@ -5655,7 +5706,6 @@ struct PlanModeStreamState { impl PlanModeStreamState { fn new(turn_id: &str) -> Self { Self { - plan_parsers: PlanParsers::new(), pending_agent_message_items: HashMap::new(), started_agent_message_items: HashSet::new(), leading_whitespace_by_item: HashMap::new(), @@ -5664,6 +5714,56 @@ impl PlanModeStreamState { } } +#[derive(Debug, Default)] +struct AssistantMessageStreamParsers { + plan_mode: bool, + parsers_by_item: HashMap, +} + +type ParsedAssistantTextDelta = AssistantTextChunk; + +impl AssistantMessageStreamParsers { + fn new(plan_mode: bool) -> Self { + Self { + plan_mode, + parsers_by_item: HashMap::new(), + } + } + + fn parser_mut(&mut self, item_id: &str) -> &mut AssistantTextStreamParser { + let plan_mode = self.plan_mode; + self.parsers_by_item + .entry(item_id.to_string()) + .or_insert_with(|| AssistantTextStreamParser::new(plan_mode)) + } + + fn seed_item_text(&mut self, item_id: &str, text: &str) -> ParsedAssistantTextDelta { + if text.is_empty() { + return ParsedAssistantTextDelta::default(); + } + self.parser_mut(item_id).push_str(text) + } + + fn parse_delta(&mut self, item_id: &str, delta: &str) -> ParsedAssistantTextDelta { + self.parser_mut(item_id).push_str(delta) + } + + fn finish_item(&mut self, item_id: &str) -> ParsedAssistantTextDelta { + let Some(mut parser) = self.parsers_by_item.remove(item_id) else { + return ParsedAssistantTextDelta::default(); + }; + parser.finish() + } + + fn drain_finished(&mut self) -> Vec<(String, ParsedAssistantTextDelta)> { + let parsers_by_item = std::mem::take(&mut self.parsers_by_item); + parsers_by_item + .into_iter() + .map(|(item_id, mut parser)| (item_id, parser.finish())) + .collect() + } +} + impl ProposedPlanItemState { fn new(turn_id: &str) -> Self { Self { @@ -5757,46 +5857,79 @@ fn realtime_text_for_event(msg: &EventMsg) -> Option { TurnItem::AgentMessage(item) => Some(agent_message_text(item)), _ => None, }, - EventMsg::ExecCommandBegin(event) => { - let command = event.command.join(" "); - Some(format!( - "Exec command started: {command}\nWorking directory: {}", - event.cwd.display() - )) - } - EventMsg::PatchApplyBegin(event) => { - let mut files: Vec = event - .changes - .keys() - .map(|path| path.display().to_string()) - .collect(); - files.sort(); - let file_list = if files.is_empty() { - "none".to_string() - } else { - files.join(", ") - }; - Some(format!( - "apply_patch started ({count} file change(s))\nFiles: {file_list}", - count = files.len() - )) - } - EventMsg::PatchApplyEnd(event) => { - let status = match event.status { - PatchApplyStatus::Completed => "completed", - PatchApplyStatus::Failed => "failed", - PatchApplyStatus::Declined => "declined", - }; - let mut text = format!("apply_patch {status}"); - if !event.stdout.is_empty() { - text.push_str(&format!("\nstdout:\n{}", event.stdout)); - } - if !event.stderr.is_empty() { - text.push_str(&format!("\nstderr:\n{}", event.stderr)); - } - Some(text) - } - _ => None, + EventMsg::Error(_) + | EventMsg::Warning(_) + | EventMsg::RealtimeConversationStarted(_) + | EventMsg::RealtimeConversationRealtime(_) + | EventMsg::RealtimeConversationClosed(_) + | EventMsg::ModelReroute(_) + | EventMsg::ContextCompacted(_) + | EventMsg::ThreadRolledBack(_) + | EventMsg::TurnStarted(_) + | EventMsg::TurnComplete(_) + | EventMsg::TokenCount(_) + | EventMsg::UserMessage(_) + | EventMsg::AgentMessageDelta(_) + | EventMsg::AgentReasoning(_) + | EventMsg::AgentReasoningDelta(_) + | EventMsg::AgentReasoningRawContent(_) + | EventMsg::AgentReasoningRawContentDelta(_) + | EventMsg::AgentReasoningSectionBreak(_) + | EventMsg::SessionConfigured(_) + | EventMsg::ThreadNameUpdated(_) + | EventMsg::McpStartupUpdate(_) + | EventMsg::McpStartupComplete(_) + | EventMsg::McpToolCallBegin(_) + | EventMsg::McpToolCallEnd(_) + | EventMsg::WebSearchBegin(_) + | EventMsg::WebSearchEnd(_) + | EventMsg::ExecCommandBegin(_) + | EventMsg::ExecCommandOutputDelta(_) + | EventMsg::TerminalInteraction(_) + | EventMsg::ExecCommandEnd(_) + | EventMsg::PatchApplyBegin(_) + | EventMsg::PatchApplyEnd(_) + | EventMsg::ViewImageToolCall(_) + | EventMsg::ExecApprovalRequest(_) + | EventMsg::RequestUserInput(_) + | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) + | EventMsg::ElicitationRequest(_) + | EventMsg::ApplyPatchApprovalRequest(_) + | EventMsg::DeprecationNotice(_) + | EventMsg::BackgroundEvent(_) + | EventMsg::UndoStarted(_) + | EventMsg::UndoCompleted(_) + | EventMsg::StreamError(_) + | EventMsg::TurnDiff(_) + | EventMsg::GetHistoryEntryResponse(_) + | EventMsg::McpListToolsResponse(_) + | EventMsg::ListCustomPromptsResponse(_) + | EventMsg::ListSkillsResponse(_) + | EventMsg::ListRemoteSkillsResponse(_) + | EventMsg::RemoteSkillDownloaded(_) + | EventMsg::SkillsUpdateAvailable + | EventMsg::PlanUpdate(_) + | EventMsg::TurnAborted(_) + | EventMsg::ShutdownComplete + | EventMsg::EnteredReviewMode(_) + | EventMsg::ExitedReviewMode(_) + | EventMsg::RawResponseItem(_) + | EventMsg::ItemStarted(_) + | EventMsg::AgentMessageContentDelta(_) + | EventMsg::PlanDelta(_) + | EventMsg::ReasoningContentDelta(_) + | EventMsg::ReasoningRawContentDelta(_) + | EventMsg::CollabAgentSpawnBegin(_) + | EventMsg::CollabAgentSpawnEnd(_) + | EventMsg::CollabAgentInteractionBegin(_) + | EventMsg::CollabAgentInteractionEnd(_) + | EventMsg::CollabWaitingBegin(_) + | EventMsg::CollabWaitingEnd(_) + | EventMsg::CollabCloseBegin(_) + | EventMsg::CollabCloseEnd(_) + | EventMsg::CollabResumeBegin(_) + | EventMsg::CollabResumeEnd(_) => None, } } @@ -5866,35 +5999,68 @@ async fn handle_plan_segments( } } -/// Flush any buffered proposed-plan segments when a specific assistant message ends. -async fn flush_proposed_plan_segments_for_item( +async fn emit_streamed_assistant_text_delta( sess: &Session, turn_context: &TurnContext, - state: &mut PlanModeStreamState, + plan_mode_state: Option<&mut PlanModeStreamState>, item_id: &str, + parsed: ParsedAssistantTextDelta, ) { - let Some(mut parser) = state.plan_parsers.take_assistant_parser(item_id) else { - return; - }; - let segments = parser.finish(); - if segments.is_empty() { + if parsed.is_empty() { return; } - handle_plan_segments(sess, turn_context, state, item_id, segments).await; + if !parsed.citations.is_empty() { + // Citation extraction is intentionally local for now; we strip citations from display text + // but do not yet surface them in protocol events. + let _citations = parsed.citations; + } + if let Some(state) = plan_mode_state { + if !parsed.plan_segments.is_empty() { + handle_plan_segments(sess, turn_context, state, item_id, parsed.plan_segments).await; + } + return; + } + if parsed.visible_text.is_empty() { + return; + } + let event = AgentMessageContentDeltaEvent { + thread_id: sess.conversation_id.to_string(), + turn_id: turn_context.sub_id.clone(), + item_id: item_id.to_string(), + delta: parsed.visible_text, + }; + sess.send_event(turn_context, EventMsg::AgentMessageContentDelta(event)) + .await; } -/// Flush any remaining assistant plan parsers when the response completes. -async fn flush_proposed_plan_segments_all( +/// Flush buffered assistant text parser state when an assistant message item ends. +async fn flush_assistant_text_segments_for_item( sess: &Session, turn_context: &TurnContext, - state: &mut PlanModeStreamState, + plan_mode_state: Option<&mut PlanModeStreamState>, + parsers: &mut AssistantMessageStreamParsers, + item_id: &str, ) { - for (item_id, mut parser) in state.plan_parsers.drain_assistant_parsers() { - let segments = parser.finish(); - if segments.is_empty() { - continue; - } - handle_plan_segments(sess, turn_context, state, &item_id, segments).await; + let parsed = parsers.finish_item(item_id); + emit_streamed_assistant_text_delta(sess, turn_context, plan_mode_state, item_id, parsed).await; +} + +/// Flush any remaining buffered assistant text parser state at response completion. +async fn flush_assistant_text_segments_all( + sess: &Session, + turn_context: &TurnContext, + mut plan_mode_state: Option<&mut PlanModeStreamState>, + parsers: &mut AssistantMessageStreamParsers, +) { + for (item_id, parsed) in parsers.drain_finished() { + emit_streamed_assistant_text_delta( + sess, + turn_context, + plan_mode_state.as_deref_mut(), + &item_id, + parsed, + ) + .await; } } @@ -5915,6 +6081,7 @@ async fn maybe_complete_plan_item_from_message( } } if let Some(plan_text) = extract_proposed_plan_text(&text) { + let (plan_text, _citations) = strip_citations(&plan_text); if !state.plan_item_state.started { state.plan_item_state.start(sess, turn_context).await; } @@ -6003,7 +6170,7 @@ async fn handle_assistant_item_done_in_plan_mode( { maybe_complete_plan_item_from_message(sess, turn_context, state, item).await; - if let Some(turn_item) = handle_non_tool_response_item(item, true).await { + if let Some(turn_item) = handle_non_tool_response_item(item, true) { emit_turn_item_in_plan_mode( sess, turn_context, @@ -6014,8 +6181,7 @@ async fn handle_assistant_item_done_in_plan_mode( .await; } - sess.record_conversation_items(turn_context, std::slice::from_ref(item)) - .await; + record_completed_response_item(sess, turn_context, item).await; if let Some(agent_message) = last_assistant_message_from_item(item, true) { *last_agent_message = Some(agent_message); } @@ -6103,6 +6269,7 @@ async fn try_run_sampling_request( let mut active_item: Option = None; let mut should_emit_turn_diff = false; let plan_mode = turn_context.collaboration_mode.mode == ModeKind::Plan; + let mut assistant_message_stream_parsers = AssistantMessageStreamParsers::new(plan_mode); let mut plan_mode_state = plan_mode.then(|| PlanModeStreamState::new(&turn_context.sub_id)); let receiving_span = trace_span!("receiving_stream"); let outcome: CodexResult = loop { @@ -6142,20 +6309,21 @@ async fn try_run_sampling_request( ResponseEvent::Created => {} ResponseEvent::OutputItemDone(item) => { let previously_active_item = active_item.take(); - if let Some(state) = plan_mode_state.as_mut() { - if let Some(previous) = previously_active_item.as_ref() { - let item_id = previous.id(); - if matches!(previous, TurnItem::AgentMessage(_)) { - flush_proposed_plan_segments_for_item( - &sess, - &turn_context, - state, - &item_id, - ) - .await; - } - } - if handle_assistant_item_done_in_plan_mode( + if let Some(previous) = previously_active_item.as_ref() + && matches!(previous, TurnItem::AgentMessage(_)) + { + let item_id = previous.id(); + flush_assistant_text_segments_for_item( + &sess, + &turn_context, + plan_mode_state.as_mut(), + &mut assistant_message_stream_parsers, + &item_id, + ) + .await; + } + if let Some(state) = plan_mode_state.as_mut() + && handle_assistant_item_done_in_plan_mode( &sess, &turn_context, &item, @@ -6164,9 +6332,8 @@ async fn try_run_sampling_request( &mut last_agent_message, ) .await - { - continue; - } + { + continue; } let mut ctx = HandleOutputCtx { @@ -6188,7 +6355,29 @@ async fn try_run_sampling_request( needs_follow_up |= output_result.needs_follow_up; } ResponseEvent::OutputItemAdded(item) => { - if let Some(turn_item) = handle_non_tool_response_item(&item, plan_mode).await { + if let Some(turn_item) = handle_non_tool_response_item(&item, plan_mode) { + let mut turn_item = turn_item; + let mut seeded_parsed: Option = None; + let mut seeded_item_id: Option = None; + if matches!(turn_item, TurnItem::AgentMessage(_)) + && let Some(raw_text) = raw_assistant_output_text_from_item(&item) + { + let item_id = turn_item.id(); + let mut seeded = + assistant_message_stream_parsers.seed_item_text(&item_id, &raw_text); + if let TurnItem::AgentMessage(agent_message) = &mut turn_item { + agent_message.content = + vec![codex_protocol::items::AgentMessageContent::Text { + text: if plan_mode { + String::new() + } else { + std::mem::take(&mut seeded.visible_text) + }, + }]; + } + seeded_parsed = plan_mode.then_some(seeded); + seeded_item_id = Some(item_id); + } if let Some(state) = plan_mode_state.as_mut() && matches!(turn_item, TurnItem::AgentMessage(_)) { @@ -6199,6 +6388,20 @@ async fn try_run_sampling_request( } else { sess.emit_turn_item_started(&turn_context, &turn_item).await; } + if let (Some(state), Some(item_id), Some(parsed)) = ( + plan_mode_state.as_mut(), + seeded_item_id.as_deref(), + seeded_parsed, + ) { + emit_streamed_assistant_text_delta( + &sess, + &turn_context, + Some(state), + item_id, + parsed, + ) + .await; + } active_item = Some(turn_item); } } @@ -6228,9 +6431,13 @@ async fn try_run_sampling_request( token_usage, can_append: _, } => { - if let Some(state) = plan_mode_state.as_mut() { - flush_proposed_plan_segments_all(&sess, &turn_context, state).await; - } + flush_assistant_text_segments_all( + &sess, + &turn_context, + plan_mode_state.as_mut(), + &mut assistant_message_stream_parsers, + ) + .await; sess.update_token_usage_info(&turn_context, token_usage.as_ref()) .await; should_emit_turn_diff = true; @@ -6247,14 +6454,16 @@ async fn try_run_sampling_request( // UI will show a selection popup from the final ReviewOutput. if let Some(active) = active_item.as_ref() { let item_id = active.id(); - if let Some(state) = plan_mode_state.as_mut() - && matches!(active, TurnItem::AgentMessage(_)) - { - let segments = state - .plan_parsers - .assistant_parser_mut(&item_id) - .parse(&delta); - handle_plan_segments(&sess, &turn_context, state, &item_id, segments).await; + if matches!(active, TurnItem::AgentMessage(_)) { + let parsed = assistant_message_stream_parsers.parse_delta(&item_id, &delta); + emit_streamed_assistant_text_delta( + &sess, + &turn_context, + plan_mode_state.as_mut(), + &item_id, + parsed, + ) + .await; } else { let event = AgentMessageContentDeltaEvent { thread_id: sess.conversation_id.to_string(), @@ -6320,6 +6529,14 @@ async fn try_run_sampling_request( } }; + flush_assistant_text_segments_all( + &sess, + &turn_context, + plan_mode_state.as_mut(), + &mut assistant_message_stream_parsers, + ) + .await; + drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?; if should_emit_turn_diff { @@ -6337,29 +6554,20 @@ async fn try_run_sampling_request( } pub(super) fn get_last_assistant_message_from_turn(responses: &[ResponseItem]) -> Option { - responses.iter().rev().find_map(|item| { - if let ResponseItem::Message { role, content, .. } = item { - if role == "assistant" { - content.iter().rev().find_map(|ci| { - if let ContentItem::OutputText { text } = ci { - Some(text.clone()) - } else { - None - } - }) - } else { - None - } - } else { - None + for item in responses.iter().rev() { + if let Some(message) = last_assistant_message_from_item(item, false) { + return Some(message); } - }) + } + None } use crate::memories::prompts::build_memory_tool_developer_instructions; #[cfg(test)] pub(crate) use tests::make_session_and_context; #[cfg(test)] +pub(crate) use tests::make_session_and_context_with_dynamic_tools_and_rx; +#[cfg(test)] pub(crate) use tests::make_session_and_context_with_rx; #[cfg(test)] pub(crate) use tests::make_session_configuration_for_tests; @@ -6475,6 +6683,68 @@ mod tests { } } + #[test] + fn assistant_message_stream_parsers_can_be_seeded_from_output_item_added_text() { + let mut parsers = AssistantMessageStreamParsers::new(false); + let item_id = "msg-1"; + + let seeded = parsers.seed_item_text(item_id, "hello doc"); + let parsed = parsers.parse_delta(item_id, "1 world"); + let tail = parsers.finish_item(item_id); + + assert_eq!(seeded.visible_text, "hello "); + assert_eq!(seeded.citations, Vec::::new()); + assert_eq!(parsed.visible_text, " world"); + assert_eq!(parsed.citations, vec!["doc1".to_string()]); + assert_eq!(tail.visible_text, ""); + assert_eq!(tail.citations, Vec::::new()); + } + + #[test] + fn assistant_message_stream_parsers_seed_buffered_prefix_stays_out_of_finish_tail() { + let mut parsers = AssistantMessageStreamParsers::new(false); + let item_id = "msg-1"; + + let seeded = parsers.seed_item_text(item_id, "hello doc world"); + let tail = parsers.finish_item(item_id); + + assert_eq!(seeded.visible_text, "hello "); + assert_eq!(seeded.citations, Vec::::new()); + assert_eq!(parsed.visible_text, " world"); + assert_eq!(parsed.citations, vec!["doc".to_string()]); + assert_eq!(tail.visible_text, ""); + assert_eq!(tail.citations, Vec::::new()); + } + + #[test] + fn assistant_message_stream_parsers_seed_plan_parser_across_added_and_delta_boundaries() { + let mut parsers = AssistantMessageStreamParsers::new(true); + let item_id = "msg-1"; + + let seeded = parsers.seed_item_text(item_id, "Intro\n\n- step\n\nOutro"); + let tail = parsers.finish_item(item_id); + + assert_eq!(seeded.visible_text, "Intro\n"); + assert_eq!( + seeded.plan_segments, + vec![ProposedPlanSegment::Normal("Intro\n".to_string())] + ); + assert_eq!(parsed.visible_text, "Outro"); + assert_eq!( + parsed.plan_segments, + vec![ + ProposedPlanSegment::ProposedPlanStart, + ProposedPlanSegment::ProposedPlanDelta("- step\n".to_string()), + ProposedPlanSegment::ProposedPlanEnd, + ProposedPlanSegment::Normal("Outro".to_string()), + ] + ); + assert_eq!(tail.visible_text, ""); + assert!(tail.plan_segments.is_empty()); + } + fn make_mcp_tool( server_name: &str, tool_name: &str, @@ -7037,6 +7307,8 @@ mod tests { let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), cwd: turn_context.cwd.clone(), + current_date: turn_context.current_date.clone(), + timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), network: None, @@ -7074,6 +7346,8 @@ mod tests { let mut previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), cwd: turn_context.cwd.clone(), + current_date: turn_context.current_date.clone(), + timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), network: None, @@ -7260,7 +7534,7 @@ mod tests { session .record_context_updates_and_set_reference_context_item(&turn_context, None) .await; - expected.extend(session.build_initial_context(&turn_context).await); + expected.extend(session.build_initial_context(&turn_context, None).await); let history_after_seed = session.clone_history().await; assert_eq!(expected, history_after_seed.raw_items()); @@ -7268,7 +7542,10 @@ mod tests { .record_context_updates_and_set_reference_context_item(&turn_context, None) .await; let history_after_second_seed = session.clone_history().await; - assert_eq!(expected, history_after_second_seed.raw_items()); + assert_eq!( + history_after_seed.raw_items(), + history_after_second_seed.raw_items() + ); } #[tokio::test] @@ -7422,7 +7699,7 @@ mod tests { let reconstruction_turn = session.new_default_turn().await; expected.extend( session - .build_initial_context(reconstruction_turn.as_ref()) + .build_initial_context(reconstruction_turn.as_ref(), None) .await, ); let history = session.state.lock().await.clone_history(); @@ -7436,6 +7713,8 @@ mod tests { let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), cwd: turn_context.cwd.clone(), + current_date: turn_context.current_date.clone(), + timezone: turn_context.timezone.clone(), approval_policy: turn_context.approval_policy.value(), sandbox_policy: turn_context.sandbox_policy.get().clone(), network: None, @@ -7465,7 +7744,7 @@ mod tests { async fn thread_rollback_drops_last_turn_from_history() { let (sess, tc, rx) = make_session_and_context_with_rx().await; - let initial_context = sess.build_initial_context(tc.as_ref()).await; + let initial_context = sess.build_initial_context(tc.as_ref(), None).await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -7536,7 +7815,7 @@ mod tests { async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns() { let (sess, tc, rx) = make_session_and_context_with_rx().await; - let initial_context = sess.build_initial_context(tc.as_ref()).await; + let initial_context = sess.build_initial_context(tc.as_ref(), None).await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -7564,7 +7843,7 @@ mod tests { async fn thread_rollback_fails_when_turn_in_progress() { let (sess, tc, rx) = make_session_and_context_with_rx().await; - let initial_context = sess.build_initial_context(tc.as_ref()).await; + let initial_context = sess.build_initial_context(tc.as_ref(), None).await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -7585,7 +7864,7 @@ mod tests { async fn thread_rollback_fails_when_num_turns_is_zero() { let (sess, tc, rx) = make_session_and_context_with_rx().await; - let initial_context = sess.build_initial_context(tc.as_ref()).await; + let initial_context = sess.build_initial_context(tc.as_ref(), None).await; sess.record_into_history(&initial_context, tc.as_ref()) .await; @@ -7638,6 +7917,8 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, dynamic_tools: Vec::new(), persist_extended_history: false, @@ -7729,6 +8010,8 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, dynamic_tools: Vec::new(), persist_extended_history: false, @@ -8039,6 +8322,8 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, dynamic_tools: Vec::new(), persist_extended_history: false, @@ -8059,6 +8344,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + CollaborationModesConfig::default(), )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); let model_info = @@ -8090,6 +8376,8 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, dynamic_tools: Vec::new(), persist_extended_history: false, @@ -8134,6 +8422,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8169,6 +8458,8 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, dynamic_tools: Vec::new(), persist_extended_history: false, @@ -8201,6 +8492,7 @@ mod tests { config.background_terminal_max_timeout, ), shell_zsh_path: None, + main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( Arc::clone(&config), Arc::clone(&auth_manager), @@ -8217,6 +8509,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, @@ -8271,9 +8564,9 @@ mod tests { (session, turn_context) } - // Like make_session_and_context, but returns Arc and the event receiver - // so tests can assert on emitted events. - pub(crate) async fn make_session_and_context_with_rx() -> ( + pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( + dynamic_tools: Vec, + ) -> ( Arc, Arc, async_channel::Receiver, @@ -8289,6 +8582,7 @@ mod tests { config.codex_home.clone(), auth_manager.clone(), None, + CollaborationModesConfig::default(), )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); @@ -8324,8 +8618,10 @@ mod tests { codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), + metrics_service_name: None, + app_server_client_name: None, session_source: SessionSource::Exec, - dynamic_tools: Vec::new(), + dynamic_tools, persist_extended_history: false, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); @@ -8356,6 +8652,7 @@ mod tests { config.background_terminal_max_timeout, ), shell_zsh_path: None, + main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( Arc::clone(&config), Arc::clone(&auth_manager), @@ -8372,6 +8669,7 @@ mod tests { otel_manager: otel_manager.clone(), models_manager: Arc::clone(&models_manager), tool_approvals: Mutex::new(ApprovalStore::default()), + execve_session_approvals: RwLock::new(HashMap::new()), skills_manager, file_watcher, agent_control, @@ -8426,6 +8724,16 @@ mod tests { (session, turn_context, rx_event) } + // Like make_session_and_context, but returns Arc and the event receiver + // so tests can assert on emitted events. + pub(crate) async fn make_session_and_context_with_rx() -> ( + Arc, + Arc, + async_channel::Receiver, + ) { + make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await + } + #[tokio::test] async fn refresh_mcp_servers_is_deferred_until_next_turn() { let (session, turn_context) = make_session_and_context().await; @@ -8578,6 +8886,42 @@ mod tests { assert!(environment_update.contains("blocked.example.com")); } + #[tokio::test] + async fn build_settings_update_items_emits_environment_item_for_time_changes() { + let (session, previous_context) = make_session_and_context().await; + let previous_context = Arc::new(previous_context); + let mut current_context = previous_context + .with_model( + previous_context.model_info.slug.clone(), + &session.services.models_manager, + ) + .await; + current_context.current_date = Some("2026-02-27".to_string()); + current_context.timezone = Some("Europe/Berlin".to_string()); + + let reference_context_item = previous_context.to_turn_context_item(); + let update_items = session.build_settings_update_items( + Some(&reference_context_item), + None, + ¤t_context, + ); + + let environment_update = update_items + .iter() + .find_map(|item| match item { + ResponseItem::Message { role, content, .. } if role == "user" => { + let [ContentItem::InputText { text }] = content.as_slice() else { + return None; + }; + text.contains("").then_some(text) + } + _ => None, + }) + .expect("environment update item should be emitted"); + assert!(environment_update.contains("2026-02-27")); + assert!(environment_update.contains("Europe/Berlin")); + } + #[tokio::test] async fn record_context_updates_and_set_reference_context_item_injects_full_context_when_baseline_missing() { @@ -8586,7 +8930,7 @@ mod tests { .record_context_updates_and_set_reference_context_item(&turn_context, None) .await; let history = session.clone_history().await; - let initial_context = session.build_initial_context(&turn_context).await; + let initial_context = session.build_initial_context(&turn_context, None).await; assert_eq!(history.raw_items().to_vec(), initial_context); let current_context = session.reference_context_item().await; @@ -8630,10 +8974,28 @@ mod tests { let history = session.clone_history().await; let mut expected_history = vec![compacted_summary]; - expected_history.extend(session.build_initial_context(&turn_context).await); + expected_history.extend(session.build_initial_context(&turn_context, None).await); assert_eq!(history.raw_items().to_vec(), expected_history); } + #[tokio::test] + async fn build_initial_context_prepends_model_switch_message() { + let (session, turn_context) = make_session_and_context().await; + + let initial_context = session + .build_initial_context(&turn_context, Some("previous-regular-model")) + .await; + + let ResponseItem::Message { role, content, .. } = &initial_context[0] else { + panic!("expected developer message"); + }; + assert_eq!(role, "developer"); + let [ContentItem::InputText { text }, ..] = content.as_slice() else { + panic!("expected developer text"); + }; + assert!(text.contains("")); + } + #[tokio::test] async fn run_user_shell_command_does_not_set_reference_context_item() { let (session, _turn_context, rx) = make_session_and_context_with_rx().await; @@ -8955,7 +9317,7 @@ mod tests { let ContentItem::InputText { text } = content_item else { return false; }; - text.contains(crate::session_prefix::TURN_ABORTED_OPEN_TAG) + text.contains(crate::contextual_user_message::TURN_ABORTED_OPEN_TAG) }) }), "expected a model-visible turn aborted marker in history after interrupt" @@ -9029,7 +9391,7 @@ mod tests { // personality_spec) matches reconstruction. let reconstruction_turn = session.new_default_turn().await; let mut initial_context = session - .build_initial_context(reconstruction_turn.as_ref()) + .build_initial_context(reconstruction_turn.as_ref(), None) .await; // Ensure personality_spec is present when Personality is enabled, so expected matches // what reconstruction produces (build_initial_context may omit it when baked into model). @@ -9207,23 +9569,20 @@ mod tests { let timeout_ms = 1000; let sandbox_permissions = SandboxPermissions::RequireEscalated; - let command = if cfg!(windows) { - vec![ - "cmd.exe".to_string(), - "/C".to_string(), - "echo hi".to_string(), - ] - } else { - vec![ - "/bin/sh".to_string(), - "-c".to_string(), - "echo hi".to_string(), - ] - }; let params = ExecParams { - command: command.clone(), - original_command: shlex::try_join(command.iter().map(String::as_str)) - .unwrap_or_else(|_| command.join(" ")), + command: if cfg!(windows) { + vec![ + "cmd.exe".to_string(), + "/C".to_string(), + "echo hi".to_string(), + ] + } else { + vec![ + "/bin/sh".to_string(), + "-c".to_string(), + "echo hi".to_string(), + ] + }, cwd: turn_context.cwd.clone(), expiration: timeout_ms.into(), env: HashMap::new(), @@ -9237,7 +9596,6 @@ mod tests { let params2 = ExecParams { sandbox_permissions: SandboxPermissions::UseDefault, command: params.command.clone(), - original_command: params.original_command.clone(), cwd: params.cwd.clone(), expiration: timeout_ms.into(), env: HashMap::new(), @@ -9270,7 +9628,6 @@ mod tests { }) .to_string(), }, - source: ToolCallSource::Direct, }) .await; @@ -9310,7 +9667,6 @@ mod tests { }) .to_string(), }, - source: ToolCallSource::Direct, }) .await; @@ -9370,7 +9726,6 @@ mod tests { }) .to_string(), }, - source: ToolCallSource::Direct, }) .await; diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 9662830c8c..0777f8e3b2 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -59,6 +59,7 @@ pub(crate) async fn run_codex_thread_interactive( parent_session.services.agent_control.clone(), Vec::new(), false, + None, ) .await?; let codex = Arc::new(codex); @@ -321,6 +322,7 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + available_decisions, .. } = event; // Race approval with cancellation and timeout to avoid hangs. @@ -334,6 +336,7 @@ async fn handle_exec_approval( network_approval_context, proposed_execpolicy_amendment, additional_permissions, + available_decisions, ); let decision = await_approval_with_cancel( approval_fut, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index b493075d4a..19a8214ee0 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -1,6 +1,7 @@ use crate::agent::AgentStatus; use crate::codex::Codex; use crate::codex::SteerInputError; +use crate::config::ConstraintResult; use crate::error::Result as CodexResult; use crate::features::Feature; use crate::file_watcher::WatchRegistration; @@ -67,6 +68,15 @@ impl CodexThread { self.codex.steer_input(input, expected_turn_id).await } + pub async fn set_app_server_client_name( + &self, + app_server_client_name: Option, + ) -> ConstraintResult<()> { + self.codex + .set_app_server_client_name(app_server_client_name) + .await + } + /// Use sparingly: this is intended to be removed soon. pub async fn submit_with_id(&self, sub: Submission) -> CodexResult<()> { self.codex.submit_with_id(sub).await diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 43ee08fd4f..7470a14895 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -7,7 +7,6 @@ use crate::client_common::ResponseEvent; use crate::codex::Session; use crate::codex::TurnContext; use crate::codex::get_last_assistant_message_from_turn; -use crate::context_manager::ContextManager; use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::protocol::CompactedItem; @@ -51,47 +50,11 @@ pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bo provider.is_openai() } -fn is_model_switch_developer_message(item: &ResponseItem) -> bool { - match item { - ResponseItem::Message { role, content, .. } if role == "developer" => { - matches!( - content.as_slice(), - [ContentItem::InputText { text }] if text.starts_with("\n") - ) - } - _ => false, - } -} - -pub(crate) fn extract_trailing_model_switch_update_for_compaction_request( - history: &mut ContextManager, -) -> Option { - let history_items = history.raw_items(); - let last_user_turn_boundary_index = history_items - .iter() - .rposition(crate::context_manager::is_user_turn_boundary); - let model_switch_index = history_items - .iter() - .enumerate() - .rev() - .find_map(|(i, item)| { - let is_trailing = last_user_turn_boundary_index.is_none_or(|boundary| i > boundary); - if is_trailing && is_model_switch_developer_message(item) { - Some(i) - } else { - None - } - })?; - let mut replacement = history_items.to_vec(); - let model_switch_item = replacement.remove(model_switch_index); - history.replace(replacement); - Some(model_switch_item) -} - pub(crate) async fn run_inline_auto_compact_task( sess: Arc, turn_context: Arc, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { let prompt = turn_context.compact_prompt().to_string(); let input = vec![UserInput::Text { @@ -100,7 +63,14 @@ pub(crate) async fn run_inline_auto_compact_task( text_elements: Vec::new(), }]; - run_compact_task_inner(sess, turn_context, input, initial_context_injection).await?; + run_compact_task_inner( + sess, + turn_context, + input, + initial_context_injection, + previous_user_turn_model, + ) + .await?; Ok(()) } @@ -120,6 +90,7 @@ pub(crate) async fn run_compact_task( turn_context, input, InitialContextInjection::DoNotInject, + None, ) .await } @@ -129,6 +100,7 @@ async fn run_compact_task_inner( turn_context: Arc, input: Vec, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(&turn_context, &compaction_item) @@ -136,10 +108,6 @@ async fn run_compact_task_inner( let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input); let mut history = sess.clone_history().await; - // Keep compaction prompts in-distribution: if a model-switch update was injected at the - // tail of history (between turns), exclude it from the compaction request payload. - let stripped_model_switch_item = - extract_trailing_model_switch_update_for_compaction_request(&mut history); history.record_items( &[initial_input_for_turn.into()], turn_context.truncation_policy, @@ -240,15 +208,12 @@ async fn run_compact_task_inner( initial_context_injection, InitialContextInjection::BeforeLastUserMessage ) { - let initial_context = sess.build_initial_context(turn_context.as_ref()).await; + let initial_context = sess + .build_initial_context(turn_context.as_ref(), previous_user_turn_model) + .await; new_history = insert_initial_context_before_last_real_user_or_summary(new_history, initial_context); } - // Reattach the stripped model-switch update only after successful compaction so the model - // still sees the switch instructions on the next real sampling request. - if let Some(model_switch_item) = stripped_model_switch_item { - new_history.push(model_switch_item); - } let ghost_snapshots: Vec = history_items .iter() .filter(|item| matches!(item, ResponseItem::GhostSnapshot { .. })) @@ -491,14 +456,18 @@ mod tests { async fn process_compacted_history_with_test_session( compacted_history: Vec, + previous_user_turn_model: Option<&str>, ) -> (Vec, Vec) { let (session, turn_context) = crate::codex::make_session_and_context().await; - let initial_context = session.build_initial_context(&turn_context).await; + let initial_context = session + .build_initial_context(&turn_context, previous_user_turn_model) + .await; let refreshed = crate::compact_remote::process_compacted_history( &session, &turn_context, compacted_history, InitialContextInjection::BeforeLastUserMessage, + previous_user_turn_model, ) .await; (refreshed, initial_context) @@ -534,107 +503,6 @@ mod tests { assert_eq!(None, joined); } - #[test] - fn extract_trailing_model_switch_update_for_compaction_request_removes_trailing_item() { - let mut history = ContextManager::new(); - history.replace(vec![ - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "USER_MESSAGE".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: "ASSISTANT_REPLY".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "\nNEW_MODEL_INSTRUCTIONS".to_string(), - }], - end_turn: None, - phase: None, - }, - ]); - - let model_switch_item = - extract_trailing_model_switch_update_for_compaction_request(&mut history); - - assert_eq!(history.raw_items().len(), 2); - assert!(model_switch_item.is_some()); - assert!( - history - .raw_items() - .iter() - .all(|item| !is_model_switch_developer_message(item)) - ); - } - - #[test] - fn extract_trailing_model_switch_update_for_compaction_request_keeps_historical_item() { - let mut history = ContextManager::new(); - history.replace(vec![ - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "FIRST_USER_MESSAGE".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "\nOLDER_MODEL_INSTRUCTIONS".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: "ASSISTANT_REPLY".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "SECOND_USER_MESSAGE".to_string(), - }], - end_turn: None, - phase: None, - }, - ]); - - let model_switch_item = - extract_trailing_model_switch_update_for_compaction_request(&mut history); - - assert_eq!(history.raw_items().len(), 4); - assert!(model_switch_item.is_none()); - assert!( - history - .raw_items() - .iter() - .any(is_model_switch_developer_message) - ); - } - #[test] fn collect_user_messages_extracts_user_text_only() { let items = vec![ @@ -802,7 +670,7 @@ do things }, ]; let (refreshed, mut expected) = - process_compacted_history_with_test_session(compacted_history).await; + process_compacted_history_with_test_session(compacted_history, None).await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), @@ -827,7 +695,7 @@ do things phase: None, }]; let (refreshed, mut expected) = - process_compacted_history_with_test_session(compacted_history).await; + process_compacted_history_with_test_session(compacted_history, None).await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), @@ -903,7 +771,7 @@ keep me updated }, ]; let (refreshed, mut expected) = - process_compacted_history_with_test_session(compacted_history).await; + process_compacted_history_with_test_session(compacted_history, None).await; expected.push(ResponseItem::Message { id: None, role: "user".to_string(), @@ -949,7 +817,7 @@ keep me updated ]; let (refreshed, initial_context) = - process_compacted_history_with_test_session(compacted_history).await; + process_compacted_history_with_test_session(compacted_history, None).await; let mut expected = vec![ ResponseItem::Message { id: None, @@ -983,6 +851,46 @@ keep me updated assert_eq!(refreshed, expected); } + #[tokio::test] + async fn process_compacted_history_reinjects_model_switch_message() { + let compacted_history = vec![ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "summary".to_string(), + }], + end_turn: None, + phase: None, + }]; + + let (refreshed, initial_context) = process_compacted_history_with_test_session( + compacted_history, + Some("previous-regular-model"), + ) + .await; + + let ResponseItem::Message { role, content, .. } = &initial_context[0] else { + panic!("expected developer message"); + }; + assert_eq!(role, "developer"); + let [ContentItem::InputText { text }, ..] = content.as_slice() else { + panic!("expected developer text"); + }; + assert!(text.contains("")); + + let mut expected = initial_context; + expected.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "summary".to_string(), + }], + end_turn: None, + phase: None, + }); + assert_eq!(refreshed, expected); + } + #[test] fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last() { let compacted_history = vec![ diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 2a9106c722..cc5f5164c3 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -4,7 +4,6 @@ use crate::Prompt; use crate::codex::Session; use crate::codex::TurnContext; use crate::compact::InitialContextInjection; -use crate::compact::extract_trailing_model_switch_update_for_compaction_request; use crate::compact::insert_initial_context_before_last_real_user_or_summary; use crate::context_manager::ContextManager; use crate::context_manager::TotalTokenUsageBreakdown; @@ -28,8 +27,15 @@ pub(crate) async fn run_inline_remote_auto_compact_task( sess: Arc, turn_context: Arc, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { - run_remote_compact_task_inner(&sess, &turn_context, initial_context_injection).await?; + run_remote_compact_task_inner( + &sess, + &turn_context, + initial_context_injection, + previous_user_turn_model, + ) + .await?; Ok(()) } @@ -44,16 +50,28 @@ pub(crate) async fn run_remote_compact_task( }); sess.send_event(&turn_context, start_event).await; - run_remote_compact_task_inner(&sess, &turn_context, InitialContextInjection::DoNotInject).await + run_remote_compact_task_inner( + &sess, + &turn_context, + InitialContextInjection::DoNotInject, + None, + ) + .await } async fn run_remote_compact_task_inner( sess: &Arc, turn_context: &Arc, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { - if let Err(err) = - run_remote_compact_task_inner_impl(sess, turn_context, initial_context_injection).await + if let Err(err) = run_remote_compact_task_inner_impl( + sess, + turn_context, + initial_context_injection, + previous_user_turn_model, + ) + .await { let event = EventMsg::Error( err.to_error_event(Some("Error running remote compact task".to_string())), @@ -68,15 +86,12 @@ async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> CodexResult<()> { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(turn_context, &compaction_item) .await; let mut history = sess.clone_history().await; - // Keep compaction prompts in-distribution: if a model-switch update was injected at the - // tail of history (between turns), exclude it from the compaction request payload. - let stripped_model_switch_item = - extract_trailing_model_switch_update_for_compaction_request(&mut history); let base_instructions = sess.get_base_instructions().await; let deleted_items = trim_function_call_history_to_fit_context_window( &mut history, @@ -90,7 +105,6 @@ async fn run_remote_compact_task_inner_impl( "trimmed history items before remote compaction" ); } - // Required to keep `/undo` available after compaction let ghost_snapshots: Vec = history .raw_items() @@ -134,13 +148,9 @@ async fn run_remote_compact_task_inner_impl( turn_context.as_ref(), new_history, initial_context_injection, + previous_user_turn_model, ) .await; - // Reattach the stripped model-switch update only after successful compaction so the model - // still sees the switch instructions on the next real sampling request. - if let Some(model_switch_item) = stripped_model_switch_item { - new_history.push(model_switch_item); - } if !ghost_snapshots.is_empty() { new_history.extend(ghost_snapshots); @@ -170,6 +180,7 @@ pub(crate) async fn process_compacted_history( turn_context: &TurnContext, mut compacted_history: Vec, initial_context_injection: InitialContextInjection, + previous_user_turn_model: Option<&str>, ) -> Vec { // Mid-turn compaction is the only path that must inject initial context above the last user // message in the replacement history. Pre-turn compaction instead injects context after the @@ -178,7 +189,8 @@ pub(crate) async fn process_compacted_history( initial_context_injection, InitialContextInjection::BeforeLastUserMessage ) { - sess.build_initial_context(turn_context).await + sess.build_initial_context(turn_context, previous_user_turn_model) + .await } else { Vec::new() }; @@ -205,10 +217,12 @@ pub(crate) async fn process_compacted_history( fn should_keep_compacted_history_item(item: &ResponseItem) -> bool { match item { ResponseItem::Message { role, .. } if role == "developer" => false, - ResponseItem::Message { role, .. } if role == "user" => matches!( - crate::event_mapping::parse_turn_item(item), - Some(TurnItem::UserMessage(_)) - ), + ResponseItem::Message { role, .. } if role == "user" => { + matches!( + crate::event_mapping::parse_turn_item(item), + Some(TurnItem::UserMessage(_)) + ) + } ResponseItem::Message { role, .. } if role == "assistant" => true, ResponseItem::Message { .. } => false, ResponseItem::Compaction { .. } => true, diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index fceb965998..592f50d907 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -195,6 +195,11 @@ mod document_helpers { { entry["scopes"] = array_from_iter(scopes.iter().cloned()); } + if let Some(resource) = &config.oauth_resource + && !resource.is_empty() + { + entry["oauth_resource"] = value(resource.clone()); + } entry } @@ -839,6 +844,30 @@ impl ConfigEditsBuilder { self } + pub fn set_realtime_microphone(mut self, microphone: Option<&str>) -> Self { + let segments = vec!["audio".to_string(), "microphone".to_string()]; + match microphone { + Some(microphone) => self.edits.push(ConfigEdit::SetPath { + segments, + value: value(microphone), + }), + None => self.edits.push(ConfigEdit::ClearPath { segments }), + } + self + } + + pub fn set_realtime_speaker(mut self, speaker: Option<&str>) -> Self { + let segments = vec!["audio".to_string(), "speaker".to_string()]; + match speaker { + Some(speaker) => self.edits.push(ConfigEdit::SetPath { + segments, + value: value(speaker), + }), + None => self.edits.push(ConfigEdit::ClearPath { segments }), + } + self + } + pub fn clear_legacy_windows_sandbox_keys(mut self) -> Self { for key in [ "experimental_windows_sandbox", @@ -1441,6 +1470,7 @@ gpt-5 = "gpt-5.1" enabled_tools: Some(vec!["one".to_string(), "two".to_string()]), disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -1465,6 +1495,7 @@ gpt-5 = "gpt-5.1" enabled_tools: None, disabled_tools: Some(vec!["forbidden".to_string()]), scopes: None, + oauth_resource: Some("https://resource.example.com".to_string()), }, ); @@ -1483,6 +1514,7 @@ bearer_token_env_var = \"TOKEN\" enabled = false startup_timeout_sec = 5.0 disabled_tools = [\"forbidden\"] +oauth_resource = \"https://resource.example.com\" [mcp_servers.http.http_headers] Z-Header = \"z\" @@ -1532,6 +1564,7 @@ foo = { command = "cmd" } enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -1578,6 +1611,7 @@ foo = { command = "cmd" } # keep me enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -1623,6 +1657,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -1669,6 +1704,7 @@ foo = { command = "cmd" } enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -1804,6 +1840,50 @@ model_reasoning_effort = "high" assert_eq!(notice, Some(true)); } + #[test] + fn blocking_builder_set_realtime_audio_persists_and_clears() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + + ConfigEditsBuilder::new(codex_home) + .set_realtime_microphone(Some("USB Mic")) + .set_realtime_speaker(Some("Desk Speakers")) + .apply_blocking() + .expect("persist realtime audio"); + + let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let config: TomlValue = toml::from_str(&raw).expect("parse config"); + let realtime_audio = config + .get("audio") + .and_then(TomlValue::as_table) + .expect("audio table should exist"); + assert_eq!( + realtime_audio.get("microphone").and_then(TomlValue::as_str), + Some("USB Mic") + ); + assert_eq!( + realtime_audio.get("speaker").and_then(TomlValue::as_str), + Some("Desk Speakers") + ); + + ConfigEditsBuilder::new(codex_home) + .set_realtime_microphone(None) + .apply_blocking() + .expect("clear realtime microphone"); + + let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let config: TomlValue = toml::from_str(&raw).expect("parse config"); + let realtime_audio = config + .get("audio") + .and_then(TomlValue::as_table) + .expect("audio table should exist"); + assert_eq!(realtime_audio.get("microphone"), None); + assert_eq!( + realtime_audio.get("speaker").and_then(TomlValue::as_str), + Some("Desk Speakers") + ); + } + #[test] fn replace_mcp_servers_blocking_clears_table_when_empty() { let tmp = tempdir().expect("tmpdir"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 1b44a7ee2f..5eb1ad4c30 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -49,8 +49,6 @@ use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; -#[cfg(target_os = "macos")] -use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions; use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS; use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS; use crate::windows_sandbox::WindowsSandboxLevelExt; @@ -66,6 +64,7 @@ use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_rmcp_client::OAuthCredentialsStoreMode; @@ -82,8 +81,6 @@ use std::path::Path; use std::path::PathBuf; #[cfg(test)] use tempfile::tempdir; -#[cfg(not(target_os = "macos"))] -type MacOsSeatbeltProfileExtensions = (); use crate::config::permissions::network_proxy_config_from_permissions; use crate::config::profile::ConfigProfile; @@ -100,6 +97,7 @@ pub mod types; pub use codex_config::Constrained; pub use codex_config::ConstraintError; pub use codex_config::ConstraintResult; +pub use codex_network_proxy::NetworkProxyAuditMetadata; pub use network_proxy_spec::NetworkProxySpec; pub use network_proxy_spec::StartedNetworkProxy; @@ -115,22 +113,11 @@ pub use codex_git::GhostSnapshotConfig; /// the context window. pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB pub(crate) const DEFAULT_AGENT_MAX_THREADS: Option = Some(6); -pub(crate) const DEFAULT_AGENT_MAX_SPAWN_DEPTH: Option = Some(2); pub(crate) const DEFAULT_AGENT_MAX_DEPTH: i32 = 1; pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; pub const CONFIG_TOML_FILE: &str = "config.toml"; -fn default_sqlite_home(sandbox_policy: &SandboxPolicy, codex_home: &Path) -> PathBuf { - if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { - let mut path = std::env::temp_dir(); - path.push("codex-sqlite"); - path - } else { - codex_home.to_path_buf() - } -} - fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?; let trimmed = raw.trim(); @@ -357,8 +344,6 @@ pub struct Config { /// Maximum number of agent threads that can be open concurrently. pub agent_max_threads: Option, - /// Maximum depth for thread-spawned subagents. - pub agent_max_spawn_depth: Option, /// Maximum runtime in seconds for agent job workers before they are failed. pub agent_job_max_runtime_seconds: Option, @@ -399,6 +384,11 @@ pub struct Config { /// When this program is invoked, arg0 will be set to `codex-linux-sandbox`. pub codex_linux_sandbox_exe: Option, + /// Path to the `codex-execve-wrapper` executable used for shell + /// escalation. This cannot be set in the config file: it must be set in + /// code via [`ConfigOverrides`]. + pub main_execve_wrapper_exe: Option, + /// Optional absolute path to the Node runtime used by `js_repl`. pub js_repl_node_path: Option, @@ -419,9 +409,9 @@ pub struct Config { /// global default"). pub plan_mode_reasoning_effort: Option, - /// If not "none", the value to use for `reasoning.summary` when making a - /// request using the Responses API. - pub model_reasoning_summary: ReasoningSummary, + /// Optional value to use for `reasoning.summary` when making a request + /// using the Responses API. When unset, the model catalog default is used. + pub model_reasoning_summary: Option, /// Optional override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option, @@ -436,6 +426,9 @@ pub struct Config { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, + /// Machine-local realtime audio device preferences used by realtime voice. + pub realtime_audio: RealtimeAudioConfig, + /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport base URL (the `Op::RealtimeConversation` `/ws` /// connection) without changing normal provider HTTP requests. @@ -646,7 +639,8 @@ impl Config { /// designed to use [AskForApproval::Never] exclusively. /// /// Further, [ConfigOverrides] contains some options that are not supported - /// in [ConfigToml], such as `cwd` and `codex_linux_sandbox_exe`. + /// in [ConfigToml], such as `cwd`, `codex_linux_sandbox_exe`, and + /// `main_execve_wrapper_exe`. pub async fn load_with_cli_overrides_and_harness_overrides( cli_overrides: Vec<(String, TomlValue)>, harness_overrides: ConfigOverrides, @@ -1143,8 +1137,7 @@ pub struct ConfigToml { pub history: Option, /// Directory where Codex stores the SQLite state DB. - /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses a temp dir - /// under WorkspaceWrite sandboxing and `$CODEX_HOME` for other modes. + /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`. pub sqlite_home: Option, /// Directory where Codex writes log files, for example `codex-tui.log`. @@ -1185,6 +1178,10 @@ pub struct ConfigToml { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: Option, + /// Machine-local realtime audio device preferences used by realtime voice. + #[serde(default)] + pub audio: Option, + /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport base URL (the `Op::RealtimeConversation` `/ws` /// connection) without changing normal provider HTTP requests. @@ -1316,6 +1313,19 @@ impl ProjectConfig { } } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RealtimeAudioConfig { + pub microphone: Option, + pub speaker: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct RealtimeAudioToml { + pub microphone: Option, + pub speaker: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct ToolsToml { @@ -1334,9 +1344,6 @@ pub struct AgentsToml { /// When unset, no limit is enforced. #[schemars(range(min = 1))] pub max_threads: Option, - /// Maximum depth for thread-spawned subagents. - #[schemars(range(min = 1))] - pub max_spawn_depth: Option, /// Maximum nesting depth allowed for spawned agent threads. /// Root sessions start at depth 0. #[schemars(range(min = 1))] @@ -1536,6 +1543,7 @@ pub struct ConfigOverrides { pub model_provider: Option, pub config_profile: Option, pub codex_linux_sandbox_exe: Option, + pub main_execve_wrapper_exe: Option, pub js_repl_node_path: Option, pub js_repl_node_module_dirs: Option>, pub zsh_path: Option, @@ -1665,6 +1673,7 @@ impl Config { model_provider, config_profile: config_profile_key, codex_linux_sandbox_exe, + main_execve_wrapper_exe, js_repl_node_path: js_repl_node_path_override, js_repl_node_module_dirs: js_repl_node_module_dirs_override, zsh_path: zsh_path_override, @@ -1857,25 +1866,6 @@ impl Config { }) .transpose()? .unwrap_or_default(); - let agent_max_spawn_depth = cfg - .agents - .as_ref() - .and_then(|agents| agents.max_spawn_depth) - .or(DEFAULT_AGENT_MAX_SPAWN_DEPTH); - if agent_max_spawn_depth == Some(0) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "agents.max_spawn_depth must be at least 1", - )); - } - if let Some(max_spawn_depth) = agent_max_spawn_depth - && max_spawn_depth > i32::MAX as usize - { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "agents.max_spawn_depth must fit within a 32-bit signed integer", - )); - } let agent_job_max_runtime_seconds = cfg .agents .as_ref() @@ -2024,7 +2014,7 @@ impl Config { .as_ref() .map(AbsolutePathBuf::to_path_buf) .or_else(|| resolve_sqlite_home_env(&resolved_cwd)) - .unwrap_or_else(|| default_sqlite_home(&sandbox_policy, &codex_home)); + .unwrap_or_else(|| codex_home.to_path_buf()); // Ensure that every field of ConfigRequirements is applied to the final // Config. @@ -2141,7 +2131,6 @@ impl Config { agent_max_depth, agent_roles, memories: cfg.memories.unwrap_or_default().into(), - agent_max_spawn_depth, agent_job_max_runtime_seconds, codex_home, sqlite_home, @@ -2151,6 +2140,7 @@ impl Config { ephemeral: ephemeral.unwrap_or_default(), file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), codex_linux_sandbox_exe, + main_execve_wrapper_exe, js_repl_node_path, js_repl_node_module_dirs, zsh_path, @@ -2168,8 +2158,7 @@ impl Config { .or(cfg.plan_mode_reasoning_effort), model_reasoning_summary: config_profile .model_reasoning_summary - .or(cfg.model_reasoning_summary) - .unwrap_or_default(), + .or(cfg.model_reasoning_summary), model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries, model_catalog, model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity), @@ -2177,6 +2166,12 @@ impl Config { .chatgpt_base_url .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), + realtime_audio: cfg + .audio + .map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig { + microphone: audio.microphone, + speaker: audio.speaker, + }), experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_base_url, experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt, forced_chatgpt_workspace_id, @@ -2438,6 +2433,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, } } @@ -2457,6 +2453,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, } } @@ -2493,7 +2490,10 @@ persistence = "none" let memories = r#" [memories] +generate_memories = false +use_memories = false max_raw_memories_for_global = 512 +max_unused_days = 21 max_rollout_age_days = 42 max_rollouts_per_startup = 9 min_rollout_idle_hours = 24 @@ -2504,7 +2504,10 @@ phase_2_model = "gpt-5" toml::from_str::(memories).expect("TOML deserialization should succeed"); assert_eq!( Some(MemoriesToml { + generate_memories: Some(false), + use_memories: Some(false), max_raw_memories_for_global: Some(512), + max_unused_days: Some(21), max_rollout_age_days: Some(42), max_rollouts_per_startup: Some(9), min_rollout_idle_hours: Some(24), @@ -2523,7 +2526,10 @@ phase_2_model = "gpt-5" assert_eq!( config.memories, MemoriesConfig { + generate_memories: false, + use_memories: false, max_raw_memories_for_global: 512, + max_unused_days: 21, max_rollout_age_days: 42, max_rollouts_per_startup: 9, min_rollout_idle_hours: 24, @@ -2974,6 +2980,23 @@ trust_level = "trusted" Ok(()) } + #[test] + fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + sandbox_mode: Some(SandboxMode::WorkspaceWrite), + ..Default::default() + }, + codex_home.path().to_path_buf(), + )?; + + assert_eq!(config.sqlite_home, codex_home.path().to_path_buf()); + + Ok(()) + } + #[test] fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -3474,6 +3497,7 @@ profile = "project" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); @@ -3631,6 +3655,7 @@ bearer_token = "secret" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -3702,6 +3727,7 @@ ZIG_VAR = "3" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -3753,6 +3779,7 @@ ZIG_VAR = "3" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -3802,6 +3829,7 @@ ZIG_VAR = "3" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -3867,6 +3895,7 @@ startup_timeout_sec = 2.0 enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); apply_blocking( @@ -3944,6 +3973,7 @@ X-Auth = "DOCS_AUTH" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -3974,6 +4004,7 @@ X-Auth = "DOCS_AUTH" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); apply_blocking( @@ -4042,6 +4073,7 @@ url = "https://example.com/mcp" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ), ( @@ -4062,6 +4094,7 @@ url = "https://example.com/mcp" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ), ]); @@ -4145,6 +4178,7 @@ url = "https://example.com/mcp" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -4190,6 +4224,7 @@ url = "https://example.com/mcp" enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -4235,6 +4270,7 @@ url = "https://example.com/mcp" enabled_tools: Some(vec!["allowed".to_string()]), disabled_tools: Some(vec!["blocked".to_string()]), scopes: None, + oauth_resource: None, }, )]); @@ -4263,6 +4299,51 @@ url = "https://example.com/mcp" Ok(()) } + #[tokio::test] + async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + let servers = BTreeMap::from([( + "docs".to_string(), + McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url: "https://example.com/mcp".to_string(), + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + enabled: true, + required: false, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth_resource: Some("https://resource.example.com".to_string()), + }, + )]); + + apply_blocking( + codex_home.path(), + None, + &[ConfigEdit::ReplaceMcpServers(servers.clone())], + )?; + + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + let serialized = std::fs::read_to_string(&config_path)?; + assert!(serialized.contains(r#"oauth_resource = "https://resource.example.com""#)); + + let loaded = load_global_mcp_servers(codex_home.path()).await?; + let docs = loaded.get("docs").expect("docs entry"); + assert_eq!( + docs.oauth_resource.as_deref(), + Some("https://resource.example.com") + ); + + Ok(()) + } + #[tokio::test] async fn set_model_updates_defaults() -> anyhow::Result<()> { let codex_home = TempDir::new()?; @@ -4478,7 +4559,6 @@ model = "gpt-5.1-codex" let cfg = ConfigToml { agents: Some(AgentsToml { max_threads: None, - max_spawn_depth: None, max_depth: None, job_max_runtime_seconds: None, roles: BTreeMap::from([( @@ -4754,7 +4834,6 @@ model_verbosity = "high" agent_max_depth: DEFAULT_AGENT_MAX_DEPTH, agent_roles: BTreeMap::new(), memories: MemoriesConfig::default(), - agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH, agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS, codex_home: fixture.codex_home(), sqlite_home: fixture.codex_home(), @@ -4765,6 +4844,7 @@ model_verbosity = "high" ephemeral: false, file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, + main_execve_wrapper_exe: None, js_repl_node_path: None, js_repl_node_module_dirs: Vec::new(), zsh_path: None, @@ -4772,12 +4852,13 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), plan_mode_reasoning_effort: None, - model_reasoning_summary: ReasoningSummary::Detailed, + model_reasoning_summary: Some(ReasoningSummary::Detailed), model_supports_reasoning_summaries: None, model_catalog: None, model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_ws_backend_prompt: None, base_instructions: None, @@ -4880,7 +4961,6 @@ model_verbosity = "high" agent_max_depth: DEFAULT_AGENT_MAX_DEPTH, agent_roles: BTreeMap::new(), memories: MemoriesConfig::default(), - agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH, agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS, codex_home: fixture.codex_home(), sqlite_home: fixture.codex_home(), @@ -4891,6 +4971,7 @@ model_verbosity = "high" ephemeral: false, file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, + main_execve_wrapper_exe: None, js_repl_node_path: None, js_repl_node_module_dirs: Vec::new(), zsh_path: None, @@ -4898,12 +4979,13 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: None, plan_mode_reasoning_effort: None, - model_reasoning_summary: ReasoningSummary::default(), + model_reasoning_summary: None, model_supports_reasoning_summaries: None, model_catalog: None, model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_ws_backend_prompt: None, base_instructions: None, @@ -5004,7 +5086,6 @@ model_verbosity = "high" agent_max_depth: DEFAULT_AGENT_MAX_DEPTH, agent_roles: BTreeMap::new(), memories: MemoriesConfig::default(), - agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH, agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS, codex_home: fixture.codex_home(), sqlite_home: fixture.codex_home(), @@ -5015,6 +5096,7 @@ model_verbosity = "high" ephemeral: false, file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, + main_execve_wrapper_exe: None, js_repl_node_path: None, js_repl_node_module_dirs: Vec::new(), zsh_path: None, @@ -5022,12 +5104,13 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: None, plan_mode_reasoning_effort: None, - model_reasoning_summary: ReasoningSummary::default(), + model_reasoning_summary: None, model_supports_reasoning_summaries: None, model_catalog: None, model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_ws_backend_prompt: None, base_instructions: None, @@ -5114,7 +5197,6 @@ model_verbosity = "high" agent_max_depth: DEFAULT_AGENT_MAX_DEPTH, agent_roles: BTreeMap::new(), memories: MemoriesConfig::default(), - agent_max_spawn_depth: DEFAULT_AGENT_MAX_SPAWN_DEPTH, agent_job_max_runtime_seconds: DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS, codex_home: fixture.codex_home(), sqlite_home: fixture.codex_home(), @@ -5125,6 +5207,7 @@ model_verbosity = "high" ephemeral: false, file_opener: UriBasedFileOpener::VsCode, codex_linux_sandbox_exe: None, + main_execve_wrapper_exe: None, js_repl_node_path: None, js_repl_node_module_dirs: Vec::new(), zsh_path: None, @@ -5132,12 +5215,13 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), plan_mode_reasoning_effort: None, - model_reasoning_summary: ReasoningSummary::Detailed, + model_reasoning_summary: Some(ReasoningSummary::Detailed), model_supports_reasoning_summaries: None, model_catalog: None, model_verbosity: Some(Verbosity::High), personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_ws_backend_prompt: None, base_instructions: None, @@ -5982,6 +6066,39 @@ experimental_realtime_ws_backend_prompt = "prompt from config" ); Ok(()) } + + #[test] + fn realtime_audio_loads_from_config_toml() -> std::io::Result<()> { + let cfg: ConfigToml = toml::from_str( + r#" +[audio] +microphone = "USB Mic" +speaker = "Desk Speakers" +"#, + ) + .expect("TOML deserialization should succeed"); + + let realtime_audio = cfg + .audio + .as_ref() + .expect("realtime audio config should be present"); + assert_eq!(realtime_audio.microphone.as_deref(), Some("USB Mic")); + assert_eq!(realtime_audio.speaker.as_deref(), Some("Desk Speakers")); + + let codex_home = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + )?; + + assert_eq!(config.realtime_audio.microphone.as_deref(), Some("USB Mic")); + assert_eq!( + config.realtime_audio.speaker.as_deref(), + Some("Desk Speakers") + ); + Ok(()) + } } #[cfg(test)] diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index cd65a4dc41..702ae5749f 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -6,6 +6,7 @@ use codex_network_proxy::ConfigState; use codex_network_proxy::NetworkDecision; use codex_network_proxy::NetworkPolicyDecider; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyAuditMetadata; use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::NetworkProxyConstraints; use codex_network_proxy::NetworkProxyHandle; @@ -106,13 +107,9 @@ impl NetworkProxySpec { policy_decider: Option>, blocked_request_observer: Option>, enable_network_approval_flow: bool, + audit_metadata: NetworkProxyAuditMetadata, ) -> std::io::Result { - let state = - build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| { - std::io::Error::other(format!("failed to build network proxy state: {err}")) - })?; - let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone())); - let state = NetworkProxyState::with_reloader(state, reloader); + let state = self.build_state_with_audit_metadata(audit_metadata)?; let mut builder = NetworkProxy::builder().state(Arc::new(state)); if enable_network_approval_flow && matches!( @@ -142,6 +139,22 @@ impl NetworkProxySpec { Ok(StartedNetworkProxy::new(proxy, handle)) } + fn build_state_with_audit_metadata( + &self, + audit_metadata: NetworkProxyAuditMetadata, + ) -> std::io::Result { + let state = + build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| { + std::io::Error::other(format!("failed to build network proxy state: {err}")) + })?; + let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone())); + Ok(NetworkProxyState::with_reloader_and_audit_metadata( + state, + reloader, + audit_metadata, + )) + } + fn apply_requirements( mut config: NetworkProxyConfig, requirements: &NetworkConstraints, @@ -205,3 +218,28 @@ impl NetworkProxySpec { (config, constraints) } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn build_state_with_audit_metadata_threads_metadata_to_state() { + let spec = NetworkProxySpec { + config: NetworkProxyConfig::default(), + constraints: NetworkProxyConstraints::default(), + }; + let metadata = NetworkProxyAuditMetadata { + conversation_id: Some("conversation-1".to_string()), + app_version: Some("1.2.3".to_string()), + user_account_id: Some("acct-1".to_string()), + ..NetworkProxyAuditMetadata::default() + }; + + let state = spec + .build_state_with_audit_metadata(metadata.clone()) + .expect("state should build"); + assert_eq!(state.audit_metadata(), &metadata); + } +} diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 678766e807..3ee85226e1 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -27,6 +27,7 @@ pub const DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP: usize = 16; pub const DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS: i64 = 30; pub const DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS: i64 = 6; pub const DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL: usize = 1_024; +pub const DEFAULT_MEMORIES_MAX_UNUSED_DAYS: i64 = 30; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "kebab-case")] @@ -98,6 +99,10 @@ pub struct McpServerConfig { /// Optional OAuth scopes to request during MCP login. #[serde(default, skip_serializing_if = "Option::is_none")] pub scopes: Option>, + + /// Optional OAuth resource parameter to include during MCP login (RFC 8707). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oauth_resource: Option, } // Raw MCP config shape used for deserialization and JSON Schema generation. @@ -142,6 +147,8 @@ pub(crate) struct RawMcpServerConfig { pub disabled_tools: Option>, #[serde(default)] pub scopes: Option>, + #[serde(default)] + pub oauth_resource: Option, } impl<'de> Deserialize<'de> for McpServerConfig { @@ -165,6 +172,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { let enabled_tools = raw.enabled_tools.clone(); let disabled_tools = raw.disabled_tools.clone(); let scopes = raw.scopes.clone(); + let oauth_resource = raw.oauth_resource.clone(); fn throw_if_set(transport: &str, field: &str, value: Option<&T>) -> Result<(), E> where @@ -188,6 +196,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?; throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?; throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?; + throw_if_set("stdio", "oauth_resource", raw.oauth_resource.as_ref())?; McpServerTransportConfig::Stdio { command, args: raw.args.clone().unwrap_or_default(), @@ -221,6 +230,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { enabled_tools, disabled_tools, scopes, + oauth_resource, }) } } @@ -361,8 +371,14 @@ pub struct FeedbackConfigToml { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct MemoriesToml { + /// When `false`, newly created threads are stored with `memory_mode = "disabled"` in the state DB. + pub generate_memories: Option, + /// When `false`, skip injecting memory usage instructions into developer prompts. + pub use_memories: Option, /// Maximum number of recent raw memories retained for global consolidation. pub max_raw_memories_for_global: Option, + /// Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection. + pub max_unused_days: Option, /// Maximum age of the threads used for memories. pub max_rollout_age_days: Option, /// Maximum number of rollout candidates processed per pass. @@ -378,7 +394,10 @@ pub struct MemoriesToml { /// Effective memories settings after defaults are applied. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MemoriesConfig { + pub generate_memories: bool, + pub use_memories: bool, pub max_raw_memories_for_global: usize, + pub max_unused_days: i64, pub max_rollout_age_days: i64, pub max_rollouts_per_startup: usize, pub min_rollout_idle_hours: i64, @@ -389,7 +408,10 @@ pub struct MemoriesConfig { impl Default for MemoriesConfig { fn default() -> Self { Self { + generate_memories: true, + use_memories: true, max_raw_memories_for_global: DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL, + max_unused_days: DEFAULT_MEMORIES_MAX_UNUSED_DAYS, max_rollout_age_days: DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS, max_rollouts_per_startup: DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP, min_rollout_idle_hours: DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS, @@ -403,10 +425,16 @@ impl From for MemoriesConfig { fn from(toml: MemoriesToml) -> Self { let defaults = Self::default(); Self { + generate_memories: toml.generate_memories.unwrap_or(defaults.generate_memories), + use_memories: toml.use_memories.unwrap_or(defaults.use_memories), max_raw_memories_for_global: toml .max_raw_memories_for_global .unwrap_or(defaults.max_raw_memories_for_global) .min(4096), + max_unused_days: toml + .max_unused_days + .unwrap_or(defaults.max_unused_days) + .clamp(0, 365), max_rollout_age_days: toml .max_rollout_age_days .unwrap_or(defaults.max_rollout_age_days) @@ -1084,6 +1112,22 @@ mod tests { ); } + #[test] + fn deserialize_streamable_http_server_config_with_oauth_resource() { + let cfg: McpServerConfig = toml::from_str( + r#" + url = "https://example.com/mcp" + oauth_resource = "https://api.example.com" + "#, + ) + .expect("should deserialize http config with oauth_resource"); + + assert_eq!( + cfg.oauth_resource, + Some("https://api.example.com".to_string()) + ); + } + #[test] fn deserialize_server_config_with_tool_filters() { let cfg: McpServerConfig = toml::from_str( @@ -1138,6 +1182,20 @@ mod tests { "#, ) .expect_err("should reject env_http_headers for stdio transport"); + + let err = toml::from_str::( + r#" + command = "echo" + oauth_resource = "https://api.example.com" + "#, + ) + .expect_err("should reject oauth_resource for stdio transport"); + + assert!( + err.to_string() + .contains("oauth_resource is not supported for stdio"), + "unexpected error: {err}" + ); } #[test] diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index f6f7198f6e..e4b7755abf 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -1,14 +1,11 @@ use crate::codex::TurnContext; use crate::context_manager::normalize; -use crate::instructions::SkillInstructions; -use crate::instructions::UserInstructions; -use crate::session_prefix::is_session_prefix; +use crate::event_mapping::is_contextual_user_message_content; use crate::truncate::TruncationPolicy; use crate::truncate::approx_token_count; use crate::truncate::approx_tokens_from_byte_count_i64; use crate::truncate::truncate_function_output_items_with_policy; use crate::truncate::truncate_text; -use crate::user_shell_command::is_user_shell_command_text; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; @@ -347,32 +344,21 @@ impl ContextManager { let policy_with_serialization_budget = policy * 1.2; match item { ResponseItem::FunctionCallOutput { call_id, output } => { - let body = match &output.body { - FunctionCallOutputBody::Text(content) => FunctionCallOutputBody::Text( - truncate_text(content, policy_with_serialization_budget), - ), - FunctionCallOutputBody::ContentItems(items) => { - FunctionCallOutputBody::ContentItems( - truncate_function_output_items_with_policy( - items, - policy_with_serialization_budget, - ), - ) - } - }; ResponseItem::FunctionCallOutput { call_id: call_id.clone(), - output: FunctionCallOutputPayload { - body, - success: output.success, - }, + output: truncate_function_output_payload( + output, + policy_with_serialization_budget, + ), } } ResponseItem::CustomToolCallOutput { call_id, output } => { - let truncated = truncate_text(output, policy_with_serialization_budget); ResponseItem::CustomToolCallOutput { call_id: call_id.clone(), - output: truncated, + output: truncate_function_output_payload( + output, + policy_with_serialization_budget, + ), } } ResponseItem::Message { .. } @@ -388,6 +374,25 @@ impl ContextManager { } } +fn truncate_function_output_payload( + output: &FunctionCallOutputPayload, + policy: TruncationPolicy, +) -> FunctionCallOutputPayload { + let body = match &output.body { + FunctionCallOutputBody::Text(content) => { + FunctionCallOutputBody::Text(truncate_text(content, policy)) + } + FunctionCallOutputBody::ContentItems(items) => FunctionCallOutputBody::ContentItems( + truncate_function_output_items_with_policy(items, policy), + ), + }; + + FunctionCallOutputPayload { + body, + success: output.success, + } +} + /// API messages include every non-system item (user/assistant messages, reasoning, /// tool calls, tool outputs, shell calls, and web-search calls). fn is_api_message(message: &ResponseItem) -> bool { @@ -511,7 +516,8 @@ fn image_data_url_estimate_adjustment(item: &ResponseItem) -> (i64, i64) { } } } - ResponseItem::FunctionCallOutput { output, .. } => { + ResponseItem::FunctionCallOutput { output, .. } + | ResponseItem::CustomToolCallOutput { output, .. } => { if let FunctionCallOutputBody::ContentItems(items) = &output.body { for content_item in items { if let FunctionCallOutputContentItem::InputImage { image_url } = content_item { @@ -554,33 +560,7 @@ pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool { return false; }; - if role != "user" { - return false; - } - - if UserInstructions::is_user_instructions(content) - || SkillInstructions::is_skill_instructions(content) - { - return false; - } - - for content_item in content { - match content_item { - ContentItem::InputText { text } => { - if is_session_prefix(text) || is_user_shell_command_text(text) { - return false; - } - } - ContentItem::OutputText { text } => { - if is_session_prefix(text) { - return false; - } - } - ContentItem::InputImage { .. } => {} - } - } - - true + role == "user" && !is_contextual_user_message_content(content) } fn user_message_positions(items: &[ResponseItem]) -> Vec { diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 3e11dae40a..798abc7675 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -67,7 +67,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem { fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem { ResponseItem::CustomToolCallOutput { call_id: call_id.to_string(), - output: output.to_string(), + output: FunctionCallOutputPayload::from_text(output.to_string()), } } @@ -279,6 +279,24 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ]), }, + ResponseItem::CustomToolCall { + id: None, + status: None, + call_id: "tool-1".to_string(), + name: "js_repl".to_string(), + input: "view_image".to_string(), + }, + ResponseItem::CustomToolCallOutput { + call_id: "tool-1".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputText { + text: "js repl result".to_string(), + }, + FunctionCallOutputContentItem::InputImage { + image_url: "https://example.com/js-repl-result.png".to_string(), + }, + ]), + }, ]; let history = create_history_with_items(items); let text_only_modalities = vec![InputModality::Text]; @@ -321,6 +339,25 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ]), }, + ResponseItem::CustomToolCall { + id: None, + status: None, + call_id: "tool-1".to_string(), + name: "js_repl".to_string(), + input: "view_image".to_string(), + }, + ResponseItem::CustomToolCallOutput { + call_id: "tool-1".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputText { + text: "js repl result".to_string(), + }, + FunctionCallOutputContentItem::InputText { + text: "image content omitted because you do not support image input" + .to_string(), + }, + ]), + }, ]; assert_eq!(stripped, expected); @@ -563,7 +600,6 @@ fn drop_last_n_user_turns_preserves_prefix() { fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { let items = vec![ user_input_text_msg("ctx"), - user_input_text_msg("do the thing"), user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), @@ -586,7 +622,6 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { let expected_prefix_and_first_turn = vec![ user_input_text_msg("ctx"), - user_input_text_msg("do the thing"), user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), @@ -608,7 +643,6 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { let expected_prefix_only = vec![ user_input_text_msg("ctx"), - user_input_text_msg("do the thing"), user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), @@ -623,7 +657,6 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { let mut history = create_history_with_items(vec![ user_input_text_msg("ctx"), - user_input_text_msg("do the thing"), user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), @@ -644,7 +677,6 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { let mut history = create_history_with_items(vec![ user_input_text_msg("ctx"), - user_input_text_msg("do the thing"), user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), @@ -676,7 +708,7 @@ fn remove_first_item_handles_custom_tool_pair() { }, ResponseItem::CustomToolCallOutput { call_id: "tool-1".to_string(), - output: "ok".to_string(), + output: FunctionCallOutputPayload::from_text("ok".to_string()), }, ]; let mut h = create_history_with_items(items); @@ -755,7 +787,7 @@ fn record_items_truncates_custom_tool_call_output_content() { let long_output = line.repeat(2_500); let item = ResponseItem::CustomToolCallOutput { call_id: "tool-200".to_string(), - output: long_output.clone(), + output: FunctionCallOutputPayload::from_text(long_output.clone()), }; history.record_items([&item], policy); @@ -763,7 +795,8 @@ fn record_items_truncates_custom_tool_call_output_content() { assert_eq!(history.items.len(), 1); match &history.items[0] { ResponseItem::CustomToolCallOutput { output, .. } => { - assert_ne!(output, &long_output); + let output = output.text_content().unwrap_or_default(); + assert_ne!(output, long_output); assert!( output.contains("tokens truncated"), "expected token-based truncation marker, got {output}" @@ -954,7 +987,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() { }, ResponseItem::CustomToolCallOutput { call_id: "tool-x".to_string(), - output: "aborted".to_string(), + output: FunctionCallOutputPayload::from_text("aborted".to_string()), }, ] ); @@ -1021,7 +1054,7 @@ fn normalize_removes_orphan_function_call_output() { fn normalize_removes_orphan_custom_tool_call_output() { let items = vec![ResponseItem::CustomToolCallOutput { call_id: "orphan-2".to_string(), - output: "ok".to_string(), + output: FunctionCallOutputPayload::from_text("ok".to_string()), }]; let mut h = create_history_with_items(items); @@ -1094,7 +1127,7 @@ fn normalize_mixed_inserts_and_removals() { }, ResponseItem::CustomToolCallOutput { call_id: "t1".to_string(), - output: "aborted".to_string(), + output: FunctionCallOutputPayload::from_text("aborted".to_string()), }, ResponseItem::LocalShellCall { id: None, @@ -1196,7 +1229,7 @@ fn normalize_removes_orphan_function_call_output_panics_in_debug() { fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() { let items = vec![ResponseItem::CustomToolCallOutput { call_id: "orphan-2".to_string(), - output: "ok".to_string(), + output: FunctionCallOutputPayload::from_text("ok".to_string()), }]; let mut h = create_history_with_items(items); h.normalize_history(&default_input_modalities()); @@ -1299,6 +1332,28 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() { assert!(estimated < raw_len); } +#[test] +fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() { + let payload = "C".repeat(50_000); + let image_url = format!("data:image/png;base64,{payload}"); + let item = ResponseItem::CustomToolCallOutput { + call_id: "call-js-repl".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputText { + text: "Screenshot captured".to_string(), + }, + FunctionCallOutputContentItem::InputImage { image_url }, + ]), + }; + + let raw_len = serde_json::to_string(&item).unwrap().len() as i64; + let estimated = estimate_response_item_model_visible_bytes(&item); + let expected = raw_len - payload.len() as i64 + IMAGE_BYTES_ESTIMATE; + + assert_eq!(estimated, expected); + assert!(estimated < raw_len); +} + #[test] fn non_base64_image_urls_are_unchanged() { let message_item = ResponseItem::Message { diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index a4fe9e64fd..572ac51fc8 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -1,7 +1,6 @@ use std::collections::HashSet; use codex_protocol::models::ContentItem; -use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseItem; @@ -35,10 +34,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { idx, ResponseItem::FunctionCallOutput { call_id: call_id.clone(), - output: FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text("aborted".to_string()), - ..Default::default() - }, + output: FunctionCallOutputPayload::from_text("aborted".to_string()), }, )); } @@ -59,7 +55,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { idx, ResponseItem::CustomToolCallOutput { call_id: call_id.clone(), - output: "aborted".to_string(), + output: FunctionCallOutputPayload::from_text("aborted".to_string()), }, )); } @@ -82,10 +78,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { idx, ResponseItem::FunctionCallOutput { call_id: call_id.clone(), - output: FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text("aborted".to_string()), - ..Default::default() - }, + output: FunctionCallOutputPayload::from_text("aborted".to_string()), }, )); } @@ -245,7 +238,8 @@ pub(crate) fn strip_images_when_unsupported( } *content = normalized_content; } - ResponseItem::FunctionCallOutput { output, .. } => { + ResponseItem::FunctionCallOutput { output, .. } + | ResponseItem::CustomToolCallOutput { output, .. } => { if let Some(content_items) = output.content_items_mut() { let mut normalized_content_items = Vec::with_capacity(content_items.len()); for content_item in content_items.iter() { diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 54c5667d4d..b9fc537ec7 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -4,6 +4,7 @@ use crate::features::Feature; use crate::shell::Shell; use codex_execpolicy::Policy; use codex_protocol::config_types::Personality; +use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; @@ -30,7 +31,7 @@ fn build_permissions_update_item( previous: Option<&TurnContextItem>, next: &TurnContext, exec_policy: &Policy, -) -> Option { +) -> Option { let prev = previous?; if prev.sandbox_policy == *next.sandbox_policy.get() && prev.approval_policy == next.approval_policy.value() @@ -38,27 +39,26 @@ fn build_permissions_update_item( return None; } - Some( - DeveloperInstructions::from_policy( - next.sandbox_policy.get(), - next.approval_policy.value(), - exec_policy, - &next.cwd, - next.features.enabled(Feature::RequestPermissions), - ) - .into(), - ) + Some(DeveloperInstructions::from_policy( + next.sandbox_policy.get(), + next.approval_policy.value(), + exec_policy, + &next.cwd, + next.features.enabled(Feature::RequestPermissions), + )) } fn build_collaboration_mode_update_item( previous: Option<&TurnContextItem>, next: &TurnContext, -) -> Option { +) -> Option { let prev = previous?; if prev.collaboration_mode.as_ref() != Some(&next.collaboration_mode) { // If the next mode has empty developer instructions, this returns None and we emit no // update, so prior collaboration instructions remain in the prompt history. - Some(DeveloperInstructions::from_collaboration_mode(&next.collaboration_mode)?.into()) + Some(DeveloperInstructions::from_collaboration_mode( + &next.collaboration_mode, + )?) } else { None } @@ -68,7 +68,7 @@ fn build_personality_update_item( previous: Option<&TurnContextItem>, next: &TurnContext, personality_feature_enabled: bool, -) -> Option { +) -> Option { if !personality_feature_enabled { return None; } @@ -82,8 +82,7 @@ fn build_personality_update_item( { let model_info = &next.model_info; let personality_message = personality_message_for(model_info, personality); - personality_message - .map(|message| DeveloperInstructions::personality_spec_message(message).into()) + personality_message.map(DeveloperInstructions::personality_spec_message) } else { None } @@ -103,7 +102,7 @@ pub(crate) fn personality_message_for( pub(crate) fn build_model_instructions_update_item( previous_user_turn_model: Option<&str>, next: &TurnContext, -) -> Option { +) -> Option { let previous_model = previous_user_turn_model?; if previous_model == next.model_info.slug { return None; @@ -114,7 +113,36 @@ pub(crate) fn build_model_instructions_update_item( return None; } - Some(DeveloperInstructions::model_switch_message(model_instructions).into()) + Some(DeveloperInstructions::model_switch_message( + model_instructions, + )) +} + +pub(crate) fn build_developer_update_item(text_sections: Vec) -> Option { + build_text_message("developer", text_sections) +} + +pub(crate) fn build_contextual_user_message(text_sections: Vec) -> Option { + build_text_message("user", text_sections) +} + +fn build_text_message(role: &str, text_sections: Vec) -> Option { + if text_sections.is_empty() { + return None; + } + + let content = text_sections + .into_iter() + .map(|text| ContentItem::InputText { text }) + .collect(); + + Some(ResponseItem::Message { + id: None, + role: role.to_string(), + content, + end_turn: None, + phase: None, + }) } pub(crate) fn build_settings_update_items( @@ -125,29 +153,26 @@ pub(crate) fn build_settings_update_items( exec_policy: &Policy, personality_feature_enabled: bool, ) -> Vec { - let mut update_items = Vec::new(); + let contextual_user_message = build_environment_update_item(previous, next, shell); + let developer_update_sections = [ + // Keep model-switch instructions first so model-specific guidance is read before + // any other context diffs on this turn. + build_model_instructions_update_item(previous_user_turn_model, next), + build_permissions_update_item(previous, next, exec_policy), + build_collaboration_mode_update_item(previous, next), + build_personality_update_item(previous, next, personality_feature_enabled), + ] + .into_iter() + .flatten() + .map(DeveloperInstructions::into_text) + .collect(); - // Keep model-switch instructions first so model-specific guidance is read before - // any other context diffs on this turn. - if let Some(model_instructions_item) = - build_model_instructions_update_item(previous_user_turn_model, next) - { - update_items.push(model_instructions_item); + let mut items = Vec::with_capacity(2); + if let Some(developer_message) = build_developer_update_item(developer_update_sections) { + items.push(developer_message); } - if let Some(env_item) = build_environment_update_item(previous, next, shell) { - update_items.push(env_item); + if let Some(contextual_user_message) = contextual_user_message { + items.push(contextual_user_message); } - if let Some(permissions_item) = build_permissions_update_item(previous, next, exec_policy) { - update_items.push(permissions_item); - } - if let Some(collaboration_mode_item) = build_collaboration_mode_update_item(previous, next) { - update_items.push(collaboration_mode_item); - } - if let Some(personality_item) = - build_personality_update_item(previous, next, personality_feature_enabled) - { - update_items.push(personality_item); - } - - update_items + items } diff --git a/codex-rs/core/src/contextual_user_message.rs b/codex-rs/core/src/contextual_user_message.rs new file mode 100644 index 0000000000..51a2d23ea9 --- /dev/null +++ b/codex-rs/core/src/contextual_user_message.rs @@ -0,0 +1,139 @@ +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG; +use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; + +pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for "; +pub(crate) const AGENTS_MD_END_MARKER: &str = ""; +pub(crate) const SKILL_OPEN_TAG: &str = ""; +pub(crate) const SKILL_CLOSE_TAG: &str = ""; +pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = ""; +pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = ""; +pub(crate) const TURN_ABORTED_OPEN_TAG: &str = ""; +pub(crate) const TURN_ABORTED_CLOSE_TAG: &str = ""; +pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = ""; +pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = ""; + +#[derive(Clone, Copy)] +pub(crate) struct ContextualUserFragmentDefinition { + start_marker: &'static str, + end_marker: &'static str, +} + +impl ContextualUserFragmentDefinition { + pub(crate) const fn new(start_marker: &'static str, end_marker: &'static str) -> Self { + Self { + start_marker, + end_marker, + } + } + + pub(crate) fn matches_text(&self, text: &str) -> bool { + let trimmed = text.trim_start(); + let starts_with_marker = trimmed + .get(..self.start_marker.len()) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker)); + let trimmed = trimmed.trim_end(); + let ends_with_marker = trimmed + .get(trimmed.len().saturating_sub(self.end_marker.len())..) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker)); + starts_with_marker && ends_with_marker + } + + pub(crate) const fn start_marker(&self) -> &'static str { + self.start_marker + } + + pub(crate) const fn end_marker(&self) -> &'static str { + self.end_marker + } + + pub(crate) fn wrap(&self, body: String) -> String { + format!("{}\n{}\n{}", self.start_marker, body, self.end_marker) + } + + pub(crate) fn into_message(self, text: String) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text }], + end_turn: None, + phase: None, + } + } +} + +pub(crate) const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER); +pub(crate) const ENVIRONMENT_CONTEXT_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new( + ENVIRONMENT_CONTEXT_OPEN_TAG, + ENVIRONMENT_CONTEXT_CLOSE_TAG, + ); +pub(crate) const SKILL_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG); +pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new( + USER_SHELL_COMMAND_OPEN_TAG, + USER_SHELL_COMMAND_CLOSE_TAG, + ); +pub(crate) const TURN_ABORTED_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new(TURN_ABORTED_OPEN_TAG, TURN_ABORTED_CLOSE_TAG); +pub(crate) const SUBAGENT_NOTIFICATION_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new( + SUBAGENT_NOTIFICATION_OPEN_TAG, + SUBAGENT_NOTIFICATION_CLOSE_TAG, + ); + +const CONTEXTUAL_USER_FRAGMENTS: &[ContextualUserFragmentDefinition] = &[ + AGENTS_MD_FRAGMENT, + ENVIRONMENT_CONTEXT_FRAGMENT, + SKILL_FRAGMENT, + USER_SHELL_COMMAND_FRAGMENT, + TURN_ABORTED_FRAGMENT, + SUBAGENT_NOTIFICATION_FRAGMENT, +]; + +pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool { + let ContentItem::InputText { text } = content_item else { + return false; + }; + CONTEXTUAL_USER_FRAGMENTS + .iter() + .any(|definition| definition.matches_text(text)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_environment_context_fragment() { + assert!(is_contextual_user_fragment(&ContentItem::InputText { + text: "\n/tmp\n".to_string(), + })); + } + + #[test] + fn detects_agents_instructions_fragment() { + assert!(is_contextual_user_fragment(&ContentItem::InputText { + text: "# AGENTS.md instructions for /tmp\n\n\nbody\n" + .to_string(), + })); + } + + #[test] + fn detects_subagent_notification_fragment_case_insensitively() { + assert!( + SUBAGENT_NOTIFICATION_FRAGMENT + .matches_text("{}") + ); + } + + #[test] + fn ignores_regular_user_text() { + assert!(!is_contextual_user_fragment(&ContentItem::InputText { + text: "hello".to_string(), + })); + } +} diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 3c6df40adb..3e9ed871e3 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -1,9 +1,7 @@ use crate::codex::TurnContext; +use crate::contextual_user_message::ENVIRONMENT_CONTEXT_FRAGMENT; use crate::shell::Shell; -use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; -use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG; -use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; use serde::Deserialize; @@ -15,7 +13,10 @@ use std::path::PathBuf; pub(crate) struct EnvironmentContext { pub cwd: Option, pub shell: Shell, + pub current_date: Option, + pub timezone: Option, pub network: Option, + pub subagents: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] @@ -25,11 +26,21 @@ pub(crate) struct NetworkContext { } impl EnvironmentContext { - pub fn new(cwd: Option, shell: Shell, network: Option) -> Self { + pub fn new( + cwd: Option, + shell: Shell, + current_date: Option, + timezone: Option, + network: Option, + subagents: Option, + ) -> Self { Self { cwd, shell, + current_date, + timezone, network, + subagents, } } @@ -39,11 +50,17 @@ impl EnvironmentContext { pub fn equals_except_shell(&self, other: &EnvironmentContext) -> bool { let EnvironmentContext { cwd, + current_date, + timezone, network, - // should compare all fields except shell + subagents, shell: _, } = other; - self.cwd == *cwd && self.network == *network + self.cwd == *cwd + && self.current_date == *current_date + && self.timezone == *timezone + && self.network == *network + && self.subagents == *subagents } pub fn diff_from_turn_context_item( @@ -58,19 +75,24 @@ impl EnvironmentContext { } else { None }; + let current_date = after.current_date.clone(); + let timezone = after.timezone.clone(); let network = if before_network != after_network { after_network } else { before_network }; - EnvironmentContext::new(cwd, shell.clone(), network) + EnvironmentContext::new(cwd, shell.clone(), current_date, timezone, network, None) } pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self { Self::new( Some(turn_context.cwd.clone()), shell.clone(), + turn_context.current_date.clone(), + turn_context.timezone.clone(), Self::network_from_turn_context(turn_context), + None, ) } @@ -78,10 +100,20 @@ impl EnvironmentContext { Self::new( Some(turn_context_item.cwd.clone()), shell.clone(), + turn_context_item.current_date.clone(), + turn_context_item.timezone.clone(), Self::network_from_turn_context_item(turn_context_item), + None, ) } + pub fn with_subagents(mut self, subagents: String) -> Self { + if !subagents.is_empty() { + self.subagents = Some(subagents); + } + self + } + fn network_from_turn_context(turn_context: &TurnContext) -> Option { let network = turn_context .config @@ -122,13 +154,19 @@ impl EnvironmentContext { /// /// ``` pub fn serialize_to_xml(self) -> String { - let mut lines = vec![ENVIRONMENT_CONTEXT_OPEN_TAG.to_string()]; + let mut lines = Vec::new(); if let Some(cwd) = self.cwd { lines.push(format!(" {}", cwd.to_string_lossy())); } let shell_name = self.shell.name(); lines.push(format!(" {shell_name}")); + if let Some(current_date) = self.current_date { + lines.push(format!(" {current_date}")); + } + if let Some(timezone) = self.timezone { + lines.push(format!(" {timezone}")); + } match self.network { Some(ref network) => { lines.push(" ".to_string()); @@ -145,22 +183,18 @@ impl EnvironmentContext { // lines.push(" ".to_string()); } } - lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string()); - lines.join("\n") + if let Some(subagents) = self.subagents { + lines.push(" ".to_string()); + lines.extend(subagents.lines().map(|line| format!(" {line}"))); + lines.push(" ".to_string()); + } + ENVIRONMENT_CONTEXT_FRAGMENT.wrap(lines.join("\n")) } } impl From for ResponseItem { fn from(ec: EnvironmentContext) -> Self { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: ec.serialize_to_xml(), - }], - end_turn: None, - phase: None, - } + ENVIRONMENT_CONTEXT_FRAGMENT.into_message(ec.serialize_to_xml()) } } @@ -183,12 +217,21 @@ mod tests { #[test] fn serialize_workspace_write_environment_context() { let cwd = test_path_buf("/repo"); - let context = EnvironmentContext::new(Some(cwd.clone()), fake_shell(), None); + let context = EnvironmentContext::new( + Some(cwd.clone()), + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + None, + ); let expected = format!( r#" {cwd} bash + 2026-02-26 + America/Los_Angeles "#, cwd = cwd.display(), ); @@ -202,13 +245,21 @@ mod tests { allowed_domains: vec!["api.example.com".to_string(), "*.openai.com".to_string()], denied_domains: vec!["blocked.example.com".to_string()], }; - let context = - EnvironmentContext::new(Some(test_path_buf("/repo")), fake_shell(), Some(network)); + let context = EnvironmentContext::new( + Some(test_path_buf("/repo")), + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + Some(network), + None, + ); let expected = format!( r#" {} bash + 2026-02-26 + America/Los_Angeles api.example.com *.openai.com @@ -223,10 +274,19 @@ mod tests { #[test] fn serialize_read_only_environment_context() { - let context = EnvironmentContext::new(None, fake_shell(), None); + let context = EnvironmentContext::new( + None, + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + None, + ); let expected = r#" bash + 2026-02-26 + America/Los_Angeles "#; assert_eq!(context.serialize_to_xml(), expected); @@ -234,10 +294,19 @@ mod tests { #[test] fn serialize_external_sandbox_environment_context() { - let context = EnvironmentContext::new(None, fake_shell(), None); + let context = EnvironmentContext::new( + None, + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + None, + ); let expected = r#" bash + 2026-02-26 + America/Los_Angeles "#; assert_eq!(context.serialize_to_xml(), expected); @@ -245,10 +314,19 @@ mod tests { #[test] fn serialize_external_sandbox_with_restricted_network_environment_context() { - let context = EnvironmentContext::new(None, fake_shell(), None); + let context = EnvironmentContext::new( + None, + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + None, + ); let expected = r#" bash + 2026-02-26 + America/Los_Angeles "#; assert_eq!(context.serialize_to_xml(), expected); @@ -256,10 +334,19 @@ mod tests { #[test] fn serialize_full_access_environment_context() { - let context = EnvironmentContext::new(None, fake_shell(), None); + let context = EnvironmentContext::new( + None, + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + None, + ); let expected = r#" bash + 2026-02-26 + America/Los_Angeles "#; assert_eq!(context.serialize_to_xml(), expected); @@ -267,23 +354,65 @@ mod tests { #[test] fn equals_except_shell_compares_cwd() { - let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None); - let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None); + let context1 = EnvironmentContext::new( + Some(PathBuf::from("/repo")), + fake_shell(), + None, + None, + None, + None, + ); + let context2 = EnvironmentContext::new( + Some(PathBuf::from("/repo")), + fake_shell(), + None, + None, + None, + None, + ); assert!(context1.equals_except_shell(&context2)); } #[test] fn equals_except_shell_ignores_sandbox_policy() { - let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None); - let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None); + let context1 = EnvironmentContext::new( + Some(PathBuf::from("/repo")), + fake_shell(), + None, + None, + None, + None, + ); + let context2 = EnvironmentContext::new( + Some(PathBuf::from("/repo")), + fake_shell(), + None, + None, + None, + None, + ); assert!(context1.equals_except_shell(&context2)); } #[test] fn equals_except_shell_compares_cwd_differences() { - let context1 = EnvironmentContext::new(Some(PathBuf::from("/repo1")), fake_shell(), None); - let context2 = EnvironmentContext::new(Some(PathBuf::from("/repo2")), fake_shell(), None); + let context1 = EnvironmentContext::new( + Some(PathBuf::from("/repo1")), + fake_shell(), + None, + None, + None, + None, + ); + let context2 = EnvironmentContext::new( + Some(PathBuf::from("/repo2")), + fake_shell(), + None, + None, + None, + None, + ); assert!(!context1.equals_except_shell(&context2)); } @@ -298,6 +427,9 @@ mod tests { shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), }, None, + None, + None, + None, ); let context2 = EnvironmentContext::new( Some(PathBuf::from("/repo")), @@ -307,8 +439,39 @@ mod tests { shell_snapshot: crate::shell::empty_shell_snapshot_receiver(), }, None, + None, + None, + None, ); assert!(context1.equals_except_shell(&context2)); } + + #[test] + fn serialize_environment_context_with_subagents() { + let context = EnvironmentContext::new( + Some(test_path_buf("/repo")), + fake_shell(), + Some("2026-02-26".to_string()), + Some("America/Los_Angeles".to_string()), + None, + Some("- agent-1: atlas\n- agent-2".to_string()), + ); + + let expected = format!( + r#" + {} + bash + 2026-02-26 + America/Los_Angeles + + - agent-1: atlas + - agent-2 + +"#, + test_path_buf("/repo").display() + ); + + assert_eq!(context.serialize_to_xml(), expected); + } } diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 16f7e1c47a..afffdb0f1d 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -18,16 +18,15 @@ use codex_protocol::user_input::UserInput; use tracing::warn; use uuid::Uuid; -use crate::instructions::SkillInstructions; -use crate::instructions::UserInstructions; -use crate::session_prefix::is_session_prefix; -use crate::user_shell_command::is_user_shell_command_text; +use crate::contextual_user_message::is_contextual_user_fragment; use crate::web_search::web_search_action_detail; +pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool { + message.iter().any(is_contextual_user_fragment) +} + fn parse_user_message(message: &[ContentItem]) -> Option { - if UserInstructions::is_user_instructions(message) - || SkillInstructions::is_skill_instructions(message) - { + if is_contextual_user_message_content(message) { return None; } @@ -44,9 +43,6 @@ fn parse_user_message(message: &[ContentItem]) -> Option { { continue; } - if is_session_prefix(text) || is_user_shell_command_text(text) { - return None; - } content.push(UserInput::Text { text: text.clone(), // Model input content does not carry UI element ranges. @@ -59,9 +55,6 @@ fn parse_user_message(message: &[ContentItem]) -> Option { }); } ContentItem::OutputText { text } => { - if is_session_prefix(text) { - return None; - } warn!("Output text in user message: {}", text); } } @@ -299,7 +292,7 @@ mod tests { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: "test_text".to_string(), + text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), }], end_turn: None, phase: None, @@ -341,6 +334,22 @@ mod tests { end_turn: None, phase: None, }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ + ContentItem::InputText { + text: "ctx".to_string(), + }, + ContentItem::InputText { + text: + "# AGENTS.md instructions for dir\n\n\nbody\n" + .to_string(), + }, + ], + end_turn: None, + phase: None, + }, ]; for item in items { diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 1d1169303c..01c559c1c5 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -62,7 +62,6 @@ pub(crate) const MAX_EXEC_OUTPUT_DELTAS_PER_CALL: usize = 10_000; #[derive(Debug)] pub struct ExecParams { pub command: Vec, - pub original_command: String, pub cwd: PathBuf, pub expiration: ExecExpiration, pub env: HashMap, @@ -181,7 +180,6 @@ pub async fn process_exec_tool_call( let ExecParams { command, - original_command: _, cwd, mut env, expiration, @@ -221,6 +219,8 @@ pub async fn process_exec_tool_call( enforce_managed_network, network: network.as_ref(), sandbox_policy_cwd: sandbox_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), use_linux_sandbox_bwrap, windows_sandbox_level, @@ -251,8 +251,6 @@ pub(crate) async fn execute_exec_env( } = env; let params = ExecParams { - original_command: shlex::try_join(command.iter().map(String::as_str)) - .unwrap_or_else(|_| command.join(" ")), command, cwd, expiration, @@ -523,9 +521,6 @@ pub(crate) mod errors { SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( "seatbelt sandbox is only available on macOS".to_string(), ), - SandboxTransformError::InvalidAdditionalPermissionsPath(path) => { - CodexErr::InvalidRequest(format!("invalid additional_permissions path: {path}")) - } } } } @@ -1125,8 +1120,6 @@ mod tests { ]; let env: HashMap = std::env::vars().collect(); let params = ExecParams { - original_command: shlex::try_join(command.iter().map(String::as_str)) - .unwrap_or_else(|_| command.join(" ")), command, cwd: std::env::current_dir()?, expiration: 500.into(), @@ -1180,8 +1173,6 @@ mod tests { let cancel_token = CancellationToken::new(); let cancel_tx = cancel_token.clone(); let params = ExecParams { - original_command: shlex::try_join(command.iter().map(String::as_str)) - .unwrap_or_else(|_| command.join(" ")), command, cwd: cwd.clone(), expiration: ExecExpiration::Cancellation(cancel_token), diff --git a/codex-rs/core/src/external_agent_config.rs b/codex-rs/core/src/external_agent_config.rs new file mode 100644 index 0000000000..1d466e6f4b --- /dev/null +++ b/codex-rs/core/src/external_agent_config.rs @@ -0,0 +1,920 @@ +use serde_json::Value as JsonValue; +use std::collections::HashSet; +use std::ffi::OsString; +use std::fs; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use toml::Value as TomlValue; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExternalAgentConfigDetectOptions { + pub include_home: bool, + pub cwds: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExternalAgentConfigMigrationItemType { + Config, + Skills, + AgentsMd, + McpServerConfig, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExternalAgentConfigMigrationItem { + pub item_type: ExternalAgentConfigMigrationItemType, + pub description: String, + pub cwd: Option, +} + +#[derive(Clone)] +pub struct ExternalAgentConfigService { + codex_home: PathBuf, + claude_home: PathBuf, +} + +impl ExternalAgentConfigService { + pub fn new(codex_home: PathBuf) -> Self { + let claude_home = default_claude_home(); + Self { + codex_home, + claude_home, + } + } + + #[cfg(test)] + fn new_for_test(codex_home: PathBuf, claude_home: PathBuf) -> Self { + Self { + codex_home, + claude_home, + } + } + + pub fn detect( + &self, + params: ExternalAgentConfigDetectOptions, + ) -> io::Result> { + let mut items = Vec::new(); + if params.include_home { + self.detect_migrations(None, &mut items)?; + } + + for cwd in params.cwds.as_deref().unwrap_or(&[]) { + let Some(repo_root) = find_repo_root(Some(cwd))? else { + continue; + }; + self.detect_migrations(Some(&repo_root), &mut items)?; + } + + Ok(items) + } + + pub fn import(&self, migration_items: Vec) -> io::Result<()> { + for migration_item in migration_items { + match migration_item.item_type { + ExternalAgentConfigMigrationItemType::Config => { + self.import_config(migration_item.cwd.as_deref())? + } + ExternalAgentConfigMigrationItemType::Skills => { + self.import_skills(migration_item.cwd.as_deref())? + } + ExternalAgentConfigMigrationItemType::AgentsMd => { + self.import_agents_md(migration_item.cwd.as_deref())? + } + ExternalAgentConfigMigrationItemType::McpServerConfig => {} + } + } + + Ok(()) + } + + fn detect_migrations( + &self, + repo_root: Option<&Path>, + items: &mut Vec, + ) -> io::Result<()> { + let cwd = repo_root.map(Path::to_path_buf); + let source_settings = repo_root.map_or_else( + || self.claude_home.join("settings.json"), + |repo_root| repo_root.join(".claude").join("settings.json"), + ); + let target_config = repo_root.map_or_else( + || self.codex_home.join("config.toml"), + |repo_root| repo_root.join(".codex").join("config.toml"), + ); + if source_settings.is_file() { + let raw_settings = fs::read_to_string(&source_settings)?; + let settings: JsonValue = serde_json::from_str(&raw_settings) + .map_err(|err| invalid_data_error(err.to_string()))?; + let migrated = build_config_from_external(&settings)?; + if !is_empty_toml_table(&migrated) { + let mut should_include = true; + if target_config.exists() { + let existing_raw = fs::read_to_string(&target_config)?; + let mut existing = if existing_raw.trim().is_empty() { + TomlValue::Table(Default::default()) + } else { + toml::from_str::(&existing_raw).map_err(|err| { + invalid_data_error(format!("invalid existing config.toml: {err}")) + })? + }; + should_include = merge_missing_toml_values(&mut existing, &migrated)?; + } + + if should_include { + items.push(ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: format!( + "Migrate {} into {}.", + source_settings.display(), + target_config.display() + ), + cwd: cwd.clone(), + }); + } + } + } + + let source_skills = repo_root.map_or_else( + || self.claude_home.join("skills"), + |repo_root| repo_root.join(".claude").join("skills"), + ); + let target_skills = repo_root.map_or_else( + || self.home_target_skills_dir(), + |repo_root| repo_root.join(".agents").join("skills"), + ); + let source_skill_names = collect_subdirectory_names(&source_skills)?; + let target_skill_names = collect_subdirectory_names(&target_skills)?; + if source_skill_names + .iter() + .any(|skill_name| !target_skill_names.contains(skill_name)) + { + items.push(ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: format!( + "Copy skill folders from {} to {}.", + source_skills.display(), + target_skills.display() + ), + cwd: cwd.clone(), + }); + } + + let source_agents_md = repo_root.map_or_else( + || self.claude_home.join("CLAUDE.md"), + |repo_root| repo_root.join("CLAUDE.md"), + ); + let target_agents_md = repo_root.map_or_else( + || self.codex_home.join("AGENTS.md"), + |repo_root| repo_root.join("AGENTS.md"), + ); + if source_agents_md.is_file() && is_missing_or_empty_text_file(&target_agents_md)? { + items.push(ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: format!( + "Import {} to {}.", + source_agents_md.display(), + target_agents_md.display() + ), + cwd, + }); + } + + Ok(()) + } + + fn home_target_skills_dir(&self) -> PathBuf { + self.codex_home + .parent() + .map(|parent| parent.join(".agents").join("skills")) + .unwrap_or_else(|| PathBuf::from(".agents").join("skills")) + } + + fn import_config(&self, cwd: Option<&Path>) -> io::Result<()> { + let (source_settings, target_config) = if let Some(repo_root) = find_repo_root(cwd)? { + ( + repo_root.join(".claude").join("settings.json"), + repo_root.join(".codex").join("config.toml"), + ) + } else if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) { + return Ok(()); + } else { + ( + self.claude_home.join("settings.json"), + self.codex_home.join("config.toml"), + ) + }; + if !source_settings.is_file() { + return Ok(()); + } + + let raw_settings = fs::read_to_string(&source_settings)?; + let settings: JsonValue = serde_json::from_str(&raw_settings) + .map_err(|err| invalid_data_error(err.to_string()))?; + let migrated = build_config_from_external(&settings)?; + if is_empty_toml_table(&migrated) { + return Ok(()); + } + + let Some(target_parent) = target_config.parent() else { + return Err(invalid_data_error("config target path has no parent")); + }; + fs::create_dir_all(target_parent)?; + if !target_config.exists() { + write_toml_file(&target_config, &migrated)?; + return Ok(()); + } + + let existing_raw = fs::read_to_string(&target_config)?; + let mut existing = if existing_raw.trim().is_empty() { + TomlValue::Table(Default::default()) + } else { + toml::from_str::(&existing_raw) + .map_err(|err| invalid_data_error(format!("invalid existing config.toml: {err}")))? + }; + + let changed = merge_missing_toml_values(&mut existing, &migrated)?; + if !changed { + return Ok(()); + } + + write_toml_file(&target_config, &existing)?; + Ok(()) + } + + fn import_skills(&self, cwd: Option<&Path>) -> io::Result<()> { + let (source_skills, target_skills) = if let Some(repo_root) = find_repo_root(cwd)? { + ( + repo_root.join(".claude").join("skills"), + repo_root.join(".agents").join("skills"), + ) + } else if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) { + return Ok(()); + } else { + ( + self.claude_home.join("skills"), + self.home_target_skills_dir(), + ) + }; + if !source_skills.is_dir() { + return Ok(()); + } + + fs::create_dir_all(&target_skills)?; + + for entry in fs::read_dir(&source_skills)? { + let entry = entry?; + let file_type = entry.file_type()?; + if !file_type.is_dir() { + continue; + } + + let target = target_skills.join(entry.file_name()); + if target.exists() { + continue; + } + + copy_dir_recursive(&entry.path(), &target)?; + } + + Ok(()) + } + + fn import_agents_md(&self, cwd: Option<&Path>) -> io::Result<()> { + let (source_agents_md, target_agents_md) = if let Some(repo_root) = find_repo_root(cwd)? { + (repo_root.join("CLAUDE.md"), repo_root.join("AGENTS.md")) + } else if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) { + return Ok(()); + } else { + ( + self.claude_home.join("CLAUDE.md"), + self.codex_home.join("AGENTS.md"), + ) + }; + if !source_agents_md.is_file() || !is_missing_or_empty_text_file(&target_agents_md)? { + return Ok(()); + } + + let Some(target_parent) = target_agents_md.parent() else { + return Err(invalid_data_error("AGENTS.md target path has no parent")); + }; + fs::create_dir_all(target_parent)?; + + rewrite_and_copy_text_file(&source_agents_md, &target_agents_md) + } +} + +fn default_claude_home() -> PathBuf { + if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { + return PathBuf::from(home).join(".claude"); + } + + PathBuf::from(".claude") +} + +fn find_repo_root(cwd: Option<&Path>) -> io::Result> { + let Some(cwd) = cwd.filter(|cwd| !cwd.as_os_str().is_empty()) else { + return Ok(None); + }; + + let mut current = if cwd.is_absolute() { + cwd.to_path_buf() + } else { + std::env::current_dir()?.join(cwd) + }; + + if !current.exists() { + return Ok(None); + } + + if current.is_file() { + let Some(parent) = current.parent() else { + return Ok(None); + }; + current = parent.to_path_buf(); + } + + let fallback = current.clone(); + loop { + let git_path = current.join(".git"); + if git_path.is_dir() || git_path.is_file() { + return Ok(Some(current)); + } + if !current.pop() { + break; + } + } + + Ok(Some(fallback)) +} + +fn collect_subdirectory_names(path: &Path) -> io::Result> { + let mut names = HashSet::new(); + if !path.is_dir() { + return Ok(names); + } + + for entry in fs::read_dir(path)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + names.insert(entry.file_name()); + } + } + + Ok(names) +} + +fn is_missing_or_empty_text_file(path: &Path) -> io::Result { + if !path.exists() { + return Ok(true); + } + if !path.is_file() { + return Ok(false); + } + + Ok(fs::read_to_string(path)?.trim().is_empty()) +} + +fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> { + fs::create_dir_all(target)?; + + for entry in fs::read_dir(source)? { + let entry = entry?; + let source_path = entry.path(); + let target_path = target.join(entry.file_name()); + let file_type = entry.file_type()?; + + if file_type.is_dir() { + copy_dir_recursive(&source_path, &target_path)?; + continue; + } + + if file_type.is_file() { + if is_skill_md(&source_path) { + rewrite_and_copy_text_file(&source_path, &target_path)?; + } else { + fs::copy(source_path, target_path)?; + } + } + } + + Ok(()) +} + +fn is_skill_md(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md")) +} + +fn rewrite_and_copy_text_file(source: &Path, target: &Path) -> io::Result<()> { + let source_contents = fs::read_to_string(source)?; + let rewritten = rewrite_claude_terms(&source_contents); + fs::write(target, rewritten) +} + +fn rewrite_claude_terms(content: &str) -> String { + let mut rewritten = replace_case_insensitive_with_boundaries(content, "claude.md", "AGENTS.md"); + for from in [ + "claude code", + "claude-code", + "claude_code", + "claudecode", + "claude", + ] { + rewritten = replace_case_insensitive_with_boundaries(&rewritten, from, "Codex"); + } + rewritten +} + +fn replace_case_insensitive_with_boundaries( + input: &str, + needle: &str, + replacement: &str, +) -> String { + let needle_lower = needle.to_ascii_lowercase(); + if needle_lower.is_empty() { + return input.to_string(); + } + + let haystack_lower = input.to_ascii_lowercase(); + let bytes = input.as_bytes(); + let mut output = String::with_capacity(input.len()); + let mut last_emitted = 0usize; + let mut search_start = 0usize; + + while let Some(relative_pos) = haystack_lower[search_start..].find(&needle_lower) { + let start = search_start + relative_pos; + let end = start + needle_lower.len(); + let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); + let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); + + if boundary_before && boundary_after { + output.push_str(&input[last_emitted..start]); + output.push_str(replacement); + last_emitted = end; + } + + search_start = start + 1; + } + + if last_emitted == 0 { + return input.to_string(); + } + + output.push_str(&input[last_emitted..]); + output +} + +fn is_word_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + +fn build_config_from_external(settings: &JsonValue) -> io::Result { + let Some(settings_obj) = settings.as_object() else { + return Err(invalid_data_error( + "external agent settings root must be an object", + )); + }; + + let mut root = toml::map::Map::new(); + + if let Some(env) = settings_obj.get("env").and_then(JsonValue::as_object) + && !env.is_empty() + { + let mut shell_policy = toml::map::Map::new(); + shell_policy.insert("inherit".to_string(), TomlValue::String("core".to_string())); + shell_policy.insert( + "set".to_string(), + TomlValue::Table(json_object_to_toml_table(env)?), + ); + root.insert( + "shell_environment_policy".to_string(), + TomlValue::Table(shell_policy), + ); + } + + if let Some(sandbox_enabled) = settings_obj + .get("sandbox") + .and_then(JsonValue::as_object) + .and_then(|sandbox| sandbox.get("enabled")) + .and_then(JsonValue::as_bool) + && sandbox_enabled + { + root.insert( + "sandbox_mode".to_string(), + TomlValue::String("workspace-write".to_string()), + ); + } + + Ok(TomlValue::Table(root)) +} + +fn json_object_to_toml_table( + object: &serde_json::Map, +) -> io::Result> { + let mut table = toml::map::Map::new(); + for (key, value) in object { + table.insert(key.clone(), json_to_toml_value(value)?); + } + Ok(table) +} + +fn json_to_toml_value(value: &JsonValue) -> io::Result { + match value { + JsonValue::Null => Ok(TomlValue::String("null".to_string())), + JsonValue::Bool(v) => Ok(TomlValue::Boolean(*v)), + JsonValue::Number(n) => { + if let Some(i) = n.as_i64() { + return Ok(TomlValue::Integer(i)); + } + if let Some(f) = n.as_f64() { + return Ok(TomlValue::Float(f)); + } + Err(invalid_data_error("unsupported JSON number")) + } + JsonValue::String(v) => Ok(TomlValue::String(v.clone())), + JsonValue::Array(values) => values + .iter() + .map(json_to_toml_value) + .collect::>>() + .map(TomlValue::Array), + JsonValue::Object(map) => json_object_to_toml_table(map).map(TomlValue::Table), + } +} + +fn merge_missing_toml_values(existing: &mut TomlValue, incoming: &TomlValue) -> io::Result { + match (existing, incoming) { + (TomlValue::Table(existing_table), TomlValue::Table(incoming_table)) => { + let mut changed = false; + for (key, incoming_value) in incoming_table { + match existing_table.get_mut(key) { + Some(existing_value) => { + if matches!( + (&*existing_value, incoming_value), + (TomlValue::Table(_), TomlValue::Table(_)) + ) && merge_missing_toml_values(existing_value, incoming_value)? + { + changed = true; + } + } + None => { + existing_table.insert(key.clone(), incoming_value.clone()); + changed = true; + } + } + } + Ok(changed) + } + _ => Err(invalid_data_error( + "expected TOML table while merging migrated config values", + )), + } +} + +fn write_toml_file(path: &Path, value: &TomlValue) -> io::Result<()> { + let serialized = toml::to_string_pretty(value) + .map_err(|err| invalid_data_error(format!("failed to serialize config.toml: {err}")))?; + fs::write(path, format!("{serialized}\n")) +} + +fn is_empty_toml_table(value: &TomlValue) -> bool { + match value { + TomlValue::Table(table) => table.is_empty(), + TomlValue::String(_) + | TomlValue::Integer(_) + | TomlValue::Float(_) + | TomlValue::Boolean(_) + | TomlValue::Datetime(_) + | TomlValue::Array(_) => false, + } +} + +fn invalid_data_error(message: impl Into) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + fn fixture_paths() -> (TempDir, PathBuf, PathBuf) { + let root = TempDir::new().expect("create tempdir"); + let claude_home = root.path().join(".claude"); + let codex_home = root.path().join(".codex"); + (root, claude_home, codex_home) + } + + fn service_for_paths(claude_home: PathBuf, codex_home: PathBuf) -> ExternalAgentConfigService { + ExternalAgentConfigService::new_for_test(codex_home, claude_home) + } + + #[test] + fn detect_home_lists_config_skills_and_agents_md() { + let (_root, claude_home, codex_home) = fixture_paths(); + let agents_skills = codex_home + .parent() + .map(|parent| parent.join(".agents").join("skills")) + .unwrap_or_else(|| PathBuf::from(".agents").join("skills")); + fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create skills"); + fs::write(claude_home.join("CLAUDE.md"), "claude rules").expect("write claude md"); + fs::write( + claude_home.join("settings.json"), + r#"{"model":"claude","env":{"FOO":"bar"}}"#, + ) + .expect("write settings"); + + let items = service_for_paths(claude_home.clone(), codex_home.clone()) + .detect(ExternalAgentConfigDetectOptions { + include_home: true, + cwds: None, + }) + .expect("detect"); + + let expected = vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: format!( + "Migrate {} into {}.", + claude_home.join("settings.json").display(), + codex_home.join("config.toml").display() + ), + cwd: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: format!( + "Copy skill folders from {} to {}.", + claude_home.join("skills").display(), + agents_skills.display() + ), + cwd: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: format!( + "Import {} to {}.", + claude_home.join("CLAUDE.md").display(), + codex_home.join("AGENTS.md").display() + ), + cwd: None, + }, + ]; + + assert_eq!(items, expected); + } + + #[test] + fn detect_repo_lists_agents_md_for_each_cwd() { + let root = TempDir::new().expect("create tempdir"); + let repo_root = root.path().join("repo"); + let nested = repo_root.join("nested").join("child"); + fs::create_dir_all(repo_root.join(".git")).expect("create git dir"); + fs::create_dir_all(&nested).expect("create nested"); + fs::write(repo_root.join("CLAUDE.md"), "Claude code guidance").expect("write source"); + + let items = service_for_paths(root.path().join(".claude"), root.path().join(".codex")) + .detect(ExternalAgentConfigDetectOptions { + include_home: false, + cwds: Some(vec![nested, repo_root.clone()]), + }) + .expect("detect"); + + let expected = vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: format!( + "Import {} to {}.", + repo_root.join("CLAUDE.md").display(), + repo_root.join("AGENTS.md").display(), + ), + cwd: Some(repo_root.clone()), + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: format!( + "Import {} to {}.", + repo_root.join("CLAUDE.md").display(), + repo_root.join("AGENTS.md").display(), + ), + cwd: Some(repo_root), + }, + ]; + + assert_eq!(items, expected); + } + + #[test] + fn import_home_migrates_supported_config_fields_skills_and_agents_md() { + let (_root, claude_home, codex_home) = fixture_paths(); + let agents_skills = codex_home + .parent() + .map(|parent| parent.join(".agents").join("skills")) + .unwrap_or_else(|| PathBuf::from(".agents").join("skills")); + fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create skills"); + fs::write( + claude_home.join("settings.json"), + r#"{"model":"claude","permissions":{"ask":["git push"]},"env":{"FOO":"bar"},"sandbox":{"enabled":true,"network":{"allowLocalBinding":true}}}"#, + ) + .expect("write settings"); + fs::write( + claude_home.join("skills").join("skill-a").join("SKILL.md"), + "Use Claude Code and CLAUDE utilities.", + ) + .expect("write skill"); + fs::write(claude_home.join("CLAUDE.md"), "Claude code guidance").expect("write agents"); + + service_for_paths(claude_home, codex_home.clone()) + .import(vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: String::new(), + cwd: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: String::new(), + cwd: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: String::new(), + cwd: None, + }, + ]) + .expect("import"); + + assert_eq!( + fs::read_to_string(codex_home.join("AGENTS.md")).expect("read agents"), + "Codex guidance" + ); + + let parsed_config: TomlValue = toml::from_str( + &fs::read_to_string(codex_home.join("config.toml")).expect("read config"), + ) + .expect("parse config"); + let expected_config: TomlValue = toml::from_str( + r#" + sandbox_mode = "workspace-write" + + [shell_environment_policy] + inherit = "core" + + [shell_environment_policy.set] + FOO = "bar" + "#, + ) + .expect("parse expected"); + assert_eq!(parsed_config, expected_config); + assert_eq!( + fs::read_to_string(agents_skills.join("skill-a").join("SKILL.md")) + .expect("read copied skill"), + "Use Codex and Codex utilities." + ); + } + + #[test] + fn import_home_skips_empty_config_migration() { + let (_root, claude_home, codex_home) = fixture_paths(); + fs::create_dir_all(&claude_home).expect("create claude home"); + fs::write( + claude_home.join("settings.json"), + r#"{"model":"claude","sandbox":{"enabled":false}}"#, + ) + .expect("write settings"); + + service_for_paths(claude_home, codex_home.clone()) + .import(vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: String::new(), + cwd: None, + }]) + .expect("import"); + + assert!(!codex_home.join("config.toml").exists()); + } + + #[test] + fn detect_home_skips_config_when_target_already_has_supported_fields() { + let (_root, claude_home, codex_home) = fixture_paths(); + fs::create_dir_all(&claude_home).expect("create claude home"); + fs::create_dir_all(&codex_home).expect("create codex home"); + fs::write( + claude_home.join("settings.json"), + r#"{"env":{"FOO":"bar"},"sandbox":{"enabled":true}}"#, + ) + .expect("write settings"); + fs::write( + codex_home.join("config.toml"), + r#" + sandbox_mode = "workspace-write" + + [shell_environment_policy] + inherit = "core" + + [shell_environment_policy.set] + FOO = "bar" + "#, + ) + .expect("write config"); + + let items = service_for_paths(claude_home, codex_home) + .detect(ExternalAgentConfigDetectOptions { + include_home: true, + cwds: None, + }) + .expect("detect"); + + assert_eq!(items, Vec::::new()); + } + + #[test] + fn detect_home_skips_skills_when_all_skill_directories_exist() { + let (_root, claude_home, codex_home) = fixture_paths(); + let agents_skills = codex_home + .parent() + .map(|parent| parent.join(".agents").join("skills")) + .unwrap_or_else(|| PathBuf::from(".agents").join("skills")); + fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create source"); + fs::create_dir_all(agents_skills.join("skill-a")).expect("create target"); + + let items = service_for_paths(claude_home, codex_home) + .detect(ExternalAgentConfigDetectOptions { + include_home: true, + cwds: None, + }) + .expect("detect"); + + assert_eq!(items, Vec::::new()); + } + + #[test] + fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() { + let root = TempDir::new().expect("create tempdir"); + let repo_root = root.path().join("repo-a"); + let repo_with_existing_target = root.path().join("repo-b"); + fs::create_dir_all(repo_root.join(".git")).expect("create git"); + fs::create_dir_all(repo_with_existing_target.join(".git")).expect("create git"); + fs::write( + repo_root.join("CLAUDE.md"), + "Claude code\nclaude\nCLAUDE-CODE\nSee CLAUDE.md\n", + ) + .expect("write source"); + fs::write(repo_with_existing_target.join("CLAUDE.md"), "new source").expect("write source"); + fs::write( + repo_with_existing_target.join("AGENTS.md"), + "keep existing target", + ) + .expect("write target"); + + service_for_paths(root.path().join(".claude"), root.path().join(".codex")) + .import(vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: String::new(), + cwd: Some(repo_root.clone()), + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: String::new(), + cwd: Some(repo_with_existing_target.clone()), + }, + ]) + .expect("import"); + + assert_eq!( + fs::read_to_string(repo_root.join("AGENTS.md")).expect("read target"), + "Codex\nCodex\nCodex\nSee AGENTS.md\n" + ); + assert_eq!( + fs::read_to_string(repo_with_existing_target.join("AGENTS.md")) + .expect("read existing target"), + "keep existing target" + ); + } + + #[test] + fn import_repo_agents_md_overwrites_empty_targets() { + let root = TempDir::new().expect("create tempdir"); + let repo_root = root.path().join("repo"); + fs::create_dir_all(repo_root.join(".git")).expect("create git"); + fs::write(repo_root.join("CLAUDE.md"), "Claude code guidance").expect("write source"); + fs::write(repo_root.join("AGENTS.md"), " \n\t").expect("write empty target"); + + service_for_paths(root.path().join(".claude"), root.path().join(".codex")) + .import(vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: String::new(), + cwd: Some(repo_root.clone()), + }]) + .expect("import"); + + assert_eq!( + fs::read_to_string(repo_root.join("AGENTS.md")).expect("read target"), + "Codex guidance" + ); + } +} diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index d3bace8519..8f8178d201 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -133,10 +133,11 @@ pub enum Feature { SkillMcpDependencyInstall, /// Prompt for missing skill env var dependencies. SkillEnvVarDependencyPrompt, - /// Emit skill approval test prompts/events. - SkillApproval, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. + /// Kept for config backward compatibility; behavior is always steer-enabled. Steer, + /// Allow request_user_input in Default collaboration mode. + DefaultModeRequestUserInput, /// Enable collaboration modes (Plan, Default). /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled. CollaborationModes, @@ -460,7 +461,11 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::JsRepl, key: "js_repl", - stage: Stage::UnderDevelopment, + stage: Stage::Experimental { + name: "JavaScript REPL", + menu_description: "Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v22.22.0 installed.", + announcement: "NEW: JavaScript REPL is now available in /experimental. Enable it, then start a new chat or restart Codex to use it.", + }, default_enabled: false, }, FeatureSpec { @@ -623,18 +628,18 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, - FeatureSpec { - id: Feature::SkillApproval, - key: "skill_approval", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, FeatureSpec { id: Feature::Steer, key: "steer", - stage: Stage::Stable, + stage: Stage::Removed, default_enabled: true, }, + FeatureSpec { + id: Feature::DefaultModeRequestUserInput, + key: "default_mode_request_user_input", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CollaborationModes, key: "collaboration_modes", @@ -795,6 +800,23 @@ mod tests { assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false); } + #[test] + fn js_repl_is_experimental_and_user_toggleable() { + let spec = Feature::JsRepl.info(); + let stage = spec.stage; + let expected_node_version = include_str!("../../node-version.txt").trim_end(); + + assert!(matches!(stage, Stage::Experimental { .. })); + assert_eq!(stage.experimental_menu_name(), Some("JavaScript REPL")); + assert_eq!( + stage.experimental_menu_description().map(str::to_owned), + Some(format!( + "Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v{expected_node_version} installed." + )) + ); + assert_eq!(Feature::JsRepl.default_enabled(), false); + } + #[test] fn collab_is_legacy_alias_for_multi_agent() { assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab)); diff --git a/codex-rs/core/src/instructions/mod.rs b/codex-rs/core/src/instructions/mod.rs index 9da925ac74..9f1d95d2f6 100644 --- a/codex-rs/core/src/instructions/mod.rs +++ b/codex-rs/core/src/instructions/mod.rs @@ -1,6 +1,5 @@ mod user_instructions; pub(crate) use user_instructions::SkillInstructions; -pub use user_instructions::USER_INSTRUCTIONS_OPEN_TAG_LEGACY; pub use user_instructions::USER_INSTRUCTIONS_PREFIX; pub(crate) use user_instructions::UserInstructions; diff --git a/codex-rs/core/src/instructions/user_instructions.rs b/codex-rs/core/src/instructions/user_instructions.rs index 525834847e..09e6e4c2f7 100644 --- a/codex-rs/core/src/instructions/user_instructions.rs +++ b/codex-rs/core/src/instructions/user_instructions.rs @@ -1,12 +1,12 @@ use serde::Deserialize; use serde::Serialize; -use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; -pub const USER_INSTRUCTIONS_OPEN_TAG_LEGACY: &str = ""; +use crate::contextual_user_message::AGENTS_MD_FRAGMENT; +use crate::contextual_user_message::SKILL_FRAGMENT; + pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for "; -pub const SKILL_INSTRUCTIONS_PREFIX: &str = " bool { - if let [ContentItem::InputText { text }] = message { - text.starts_with(USER_INSTRUCTIONS_PREFIX) - || text.starts_with(USER_INSTRUCTIONS_OPEN_TAG_LEGACY) - } else { - false - } + pub(crate) fn serialize_to_text(&self) -> String { + format!( + "{prefix}{directory}\n\n\n{contents}\n{suffix}", + prefix = AGENTS_MD_FRAGMENT.start_marker(), + directory = self.directory, + contents = self.text, + suffix = AGENTS_MD_FRAGMENT.end_marker(), + ) } } impl From for ResponseItem { fn from(ui: UserInstructions) -> Self { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: format!( - "{USER_INSTRUCTIONS_PREFIX}{directory}\n\n\n{contents}\n", - directory = ui.directory, - contents = ui.text - ), - }], - end_turn: None, - phase: None, - } + AGENTS_MD_FRAGMENT.into_message(ui.serialize_to_text()) } } @@ -52,36 +41,21 @@ pub(crate) struct SkillInstructions { pub contents: String, } -impl SkillInstructions { - pub fn is_skill_instructions(message: &[ContentItem]) -> bool { - if let [ContentItem::InputText { text }] = message { - text.starts_with(SKILL_INSTRUCTIONS_PREFIX) - } else { - false - } - } -} +impl SkillInstructions {} impl From for ResponseItem { fn from(si: SkillInstructions) -> Self { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: format!( - "\n{}\n{}\n{}\n", - si.name, si.path, si.contents - ), - }], - end_turn: None, - phase: None, - } + SKILL_FRAGMENT.into_message(SKILL_FRAGMENT.wrap(format!( + "{}\n{}\n{}", + si.name, si.path, si.contents + ))) } } #[cfg(test)] mod tests { use super::*; + use codex_protocol::models::ContentItem; use pretty_assertions::assert_eq; #[test] @@ -110,21 +84,10 @@ mod tests { #[test] fn test_is_user_instructions() { - assert!(UserInstructions::is_user_instructions( - &[ContentItem::InputText { - text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), - }] + assert!(AGENTS_MD_FRAGMENT.matches_text( + "# AGENTS.md instructions for test_directory\n\n\ntest_text\n" )); - assert!(UserInstructions::is_user_instructions(&[ - ContentItem::InputText { - text: "test_text".to_string(), - } - ])); - assert!(!UserInstructions::is_user_instructions(&[ - ContentItem::InputText { - text: "test_text".to_string(), - } - ])); + assert!(!AGENTS_MD_FRAGMENT.matches_text("test_text")); } #[test] @@ -154,16 +117,9 @@ mod tests { #[test] fn test_is_skill_instructions() { - assert!(SkillInstructions::is_skill_instructions(&[ - ContentItem::InputText { - text: "\ndemo-skill\nskills/demo/SKILL.md\nbody\n" - .to_string(), - } - ])); - assert!(!SkillInstructions::is_skill_instructions(&[ - ContentItem::InputText { - text: "regular text".to_string(), - } - ])); + assert!(SKILL_FRAGMENT.matches_text( + "\ndemo-skill\nskills/demo/SKILL.md\nbody\n" + )); + assert!(!SKILL_FRAGMENT.matches_text("regular text")); } } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index a9ca37e700..7edb94b793 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -27,6 +27,7 @@ pub mod config; pub mod config_loader; pub mod connectors; mod context_manager; +mod contextual_user_message; pub mod custom_prompts; pub mod env; mod environment_context; @@ -34,6 +35,7 @@ pub mod error; pub mod exec; pub mod exec_env; mod exec_policy; +pub mod external_agent_config; pub mod features; mod file_watcher; mod flags; @@ -55,13 +57,11 @@ mod message_history; mod model_provider_info; pub mod path_utils; pub mod personality_migration; -mod proposed_plan_parser; mod sandbox_tags; pub mod sandboxing; mod session_prefix; mod shell_detect; mod stream_events_utils; -mod tagged_block_parser; pub mod test_support; mod text_encoding; pub mod token_data; diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 3abe4e9233..d744010469 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -139,6 +139,7 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, } } diff --git a/codex-rs/core/src/mcp/skill_dependencies.rs b/codex-rs/core/src/mcp/skill_dependencies.rs index 7ea040aede..8d5fc3c1a3 100644 --- a/codex-rs/core/src/mcp/skill_dependencies.rs +++ b/codex-rs/core/src/mcp/skill_dependencies.rs @@ -241,6 +241,7 @@ pub(crate) async fn maybe_install_mcp_dependencies( oauth_config.http_headers, oauth_config.env_http_headers, &[], + server_config.oauth_resource.as_deref(), config.mcp_oauth_callback_port, config.mcp_oauth_callback_url.as_deref(), ) @@ -387,6 +388,7 @@ fn mcp_dependency_to_server_config( enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }); } @@ -411,6 +413,7 @@ fn mcp_dependency_to_server_config( enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }); } @@ -433,6 +436,7 @@ mod tests { interface: None, dependencies: Some(SkillDependencies { tools }), policy: None, + permission_profile: None, permissions: None, path_to_skills_md: PathBuf::from("skill"), scope: SkillScope::User, @@ -467,6 +471,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); @@ -515,6 +520,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, )]); diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 12e3d325d3..2d22351d1f 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -2115,6 +2115,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, auth_status: McpAuthStatus::Unsupported, }; @@ -2162,6 +2163,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, auth_status: McpAuthStatus::Unsupported, }; diff --git a/codex-rs/core/src/memories/README.md b/codex-rs/core/src/memories/README.md index c19a0c6806..0a49b58316 100644 --- a/codex-rs/core/src/memories/README.md +++ b/codex-rs/core/src/memories/README.md @@ -59,7 +59,14 @@ Phase 2 consolidates the latest stage-1 outputs into the filesystem memory artif What it does: - claims a single global phase-2 job (so only one consolidation runs at a time) -- loads a bounded set of the most recent stage-1 outputs from the state DB (the per-rollout memories produced by Phase 1, used as the consolidation input set) +- loads a bounded set of stage-1 outputs from the state DB using phase-2 + selection rules: + - ignores memories whose `last_usage` falls outside the configured + `max_unused_days` window + - for memories with no `last_usage`, falls back to `generated_at` so fresh + never-used memories can still be selected + - ranks eligible memories by `usage_count` first, then by the most recent + `last_usage` / `generated_at` - computes a completion watermark from the claimed watermark + newest input timestamps - syncs local memory artifacts under the memories root: - `raw_memories.md` (merged raw memories, latest first) @@ -70,11 +77,31 @@ What it does: If there is input, it then: - spawns an internal consolidation sub-agent +- builds the Phase 2 prompt with a diff of the current Phase 1 input + selection versus the last successful Phase 2 selection (`added`, + `retained`, `removed`) - runs it with no approvals, no network, and local write access only - disables collab for that agent (to prevent recursive delegation) - watches the agent status and heartbeats the global job lease while it runs - marks the phase-2 job success/failure in the state DB when the agent finishes +Selection diff behavior: + +- successful Phase 2 runs mark the exact stage-1 snapshots they consumed with + `selected_for_phase2 = 1` and persist the matching + `selected_for_phase2_source_updated_at` +- Phase 1 upserts preserve the previous `selected_for_phase2` baseline until + the next successful Phase 2 run rewrites it +- the next Phase 2 run compares the current top-N stage-1 inputs against that + prior snapshot selection to label inputs as `added` or `retained`; a + refreshed thread stays `added` until Phase 2 successfully selects its newer + snapshot +- rows that were previously selected but still exist outside the current top-N + selection are surfaced as `removed` +- before the agent starts, local `rollout_summaries/` and `raw_memories.md` + keep the union of the current selection and the previous successful + selection, so removed-thread evidence stays available during forgetting + Watermark behavior: - The global phase-2 job claim includes an input watermark representing the latest input timestamp known when the job was claimed. diff --git a/codex-rs/core/src/memories/citations.rs b/codex-rs/core/src/memories/citations.rs new file mode 100644 index 0000000000..91c7778266 --- /dev/null +++ b/codex-rs/core/src/memories/citations.rs @@ -0,0 +1,62 @@ +use codex_protocol::ThreadId; + +pub fn get_thread_id_from_citations(citations: Vec) -> Vec { + let mut result = Vec::new(); + for citation in citations { + let mut ids_block = None; + for (open, close) in [ + ("", ""), + ("", ""), + ] { + if let Some((_, rest)) = citation.split_once(open) + && let Some((ids, _)) = rest.split_once(close) + { + ids_block = Some(ids); + break; + } + } + + if let Some(ids_block) = ids_block { + for id in ids_block + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + if let Ok(thread_id) = ThreadId::try_from(id) { + result.push(thread_id); + } + } + } + } + result +} + +#[cfg(test)] +mod tests { + use super::get_thread_id_from_citations; + use codex_protocol::ThreadId; + use pretty_assertions::assert_eq; + + #[test] + fn get_thread_id_from_citations_extracts_thread_ids() { + let first = ThreadId::new(); + let second = ThreadId::new(); + + let citations = vec![format!( + "\n\nMEMORY.md:1-2|note=[x]\n\n\n{first}\nnot-a-uuid\n{second}\n\n" + )]; + + assert_eq!(get_thread_id_from_citations(citations), vec![first, second]); + } + + #[test] + fn get_thread_id_from_citations_supports_legacy_rollout_ids() { + let thread_id = ThreadId::new(); + + let citations = vec![format!( + "\n\n{thread_id}\n\n" + )]; + + assert_eq!(get_thread_id_from_citations(citations), vec![thread_id]); + } +} diff --git a/codex-rs/core/src/memories/mod.rs b/codex-rs/core/src/memories/mod.rs index 1f892646ce..a90eba6bec 100644 --- a/codex-rs/core/src/memories/mod.rs +++ b/codex-rs/core/src/memories/mod.rs @@ -4,6 +4,7 @@ //! - Phase 1: select rollouts, extract stage-1 raw memories, persist stage-1 outputs, and enqueue consolidation. //! - Phase 2: claim a global consolidation lock, materialize consolidation inputs, and dispatch one consolidation agent. +pub(crate) mod citations; mod phase1; mod phase2; pub(crate) mod prompts; diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index 6794a4574f..fb6e99d2df 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -8,6 +8,7 @@ use crate::memories::metrics; use crate::memories::phase_two; use crate::memories::prompts::build_consolidation_prompt; use crate::memories::storage::rebuild_raw_memories_file_from_memories; +use crate::memories::storage::rollout_summary_file_stem; use crate::memories::storage::sync_rollout_summaries_from_memories; use codex_config::Constrained; use codex_protocol::ThreadId; @@ -17,8 +18,10 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::user_input::UserInput; +use codex_state::Stage1Output; use codex_state::StateRuntime; use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; use tokio::sync::watch; @@ -50,6 +53,7 @@ pub(super) async fn run(session: &Arc, config: Arc) { }; let root = memory_root(&config.codex_home); let max_raw_memories = config.memories.max_raw_memories_for_global; + let max_unused_days = config.memories.max_unused_days; // 1. Claim the job. let claim = match job::claim(session, db).await { @@ -73,21 +77,27 @@ pub(super) async fn run(session: &Arc, config: Arc) { }; // 3. Query the memories - let raw_memories = match db.list_stage1_outputs_for_global(max_raw_memories).await { - Ok(memories) => memories, + let selection = match db + .get_phase2_input_selection(max_raw_memories, max_unused_days) + .await + { + Ok(selection) => selection, Err(err) => { tracing::error!("failed to list stage1 outputs from global: {}", err); job::failed(session, db, &claim, "failed_load_stage1_outputs").await; return; } }; + let raw_memories = selection.selected.to_vec(); + let artifact_memories = artifact_memories_for_phase2(&selection); let new_watermark = get_watermark(claim.watermark, &raw_memories); // 4. Update the file system by syncing the raw memories with the one extracted from DB at // step 3 // [`rollout_summaries/`] if let Err(err) = - sync_rollout_summaries_from_memories(&root, &raw_memories, max_raw_memories).await + sync_rollout_summaries_from_memories(&root, &artifact_memories, artifact_memories.len()) + .await { tracing::error!("failed syncing local memory artifacts for global consolidation: {err}"); job::failed(session, db, &claim, "failed_sync_artifacts").await; @@ -95,7 +105,8 @@ pub(super) async fn run(session: &Arc, config: Arc) { } // [`raw_memories.md`] if let Err(err) = - rebuild_raw_memories_file_from_memories(&root, &raw_memories, max_raw_memories).await + rebuild_raw_memories_file_from_memories(&root, &artifact_memories, artifact_memories.len()) + .await { tracing::error!("failed syncing local memory artifacts for global consolidation: {err}"); job::failed(session, db, &claim, "failed_rebuild_raw_memories").await; @@ -103,12 +114,20 @@ pub(super) async fn run(session: &Arc, config: Arc) { } if raw_memories.is_empty() { // We check only after sync of the file system. - job::succeed(session, db, &claim, new_watermark, "succeeded_no_input").await; + job::succeed( + session, + db, + &claim, + new_watermark, + &[], + "succeeded_no_input", + ) + .await; return; } // 5. Spawn the agent - let prompt = agent::get_prompt(config); + let prompt = agent::get_prompt(config, &selection); let source = SessionSource::SubAgent(SubAgentSource::MemoryConsolidation); let thread_id = match session .services @@ -129,6 +148,7 @@ pub(super) async fn run(session: &Arc, config: Arc) { session, claim, new_watermark, + raw_memories.clone(), thread_id, phase_two_e2e_timer, ); @@ -140,6 +160,22 @@ pub(super) async fn run(session: &Arc, config: Arc) { emit_metrics(session, counters); } +fn artifact_memories_for_phase2( + selection: &codex_state::Phase2InputSelection, +) -> Vec { + let mut seen = HashSet::new(); + let mut memories = selection.selected.clone(); + for memory in &selection.selected { + seen.insert(rollout_summary_file_stem(memory)); + } + for memory in &selection.previous_selected { + if seen.insert(rollout_summary_file_stem(memory)) { + memories.push(memory.clone()); + } + } + memories +} + mod job { use super::*; @@ -205,6 +241,7 @@ mod job { db: &StateRuntime, claim: &Claim, completion_watermark: i64, + selected_outputs: &[codex_state::Stage1Output], reason: &'static str, ) { session.services.otel_manager.counter( @@ -213,7 +250,7 @@ mod job { &[("status", reason)], ); let _ = db - .mark_global_phase2_job_succeeded(&claim.token, completion_watermark) + .mark_global_phase2_job_succeeded(&claim.token, completion_watermark, selected_outputs) .await; } } @@ -266,9 +303,12 @@ mod agent { Some(agent_config) } - pub(super) fn get_prompt(config: Arc) -> Vec { + pub(super) fn get_prompt( + config: Arc, + selection: &codex_state::Phase2InputSelection, + ) -> Vec { let root = memory_root(&config.codex_home); - let prompt = build_consolidation_prompt(&root); + let prompt = build_consolidation_prompt(&root, selection); vec![UserInput::Text { text: prompt, text_elements: vec![], @@ -280,6 +320,7 @@ mod agent { session: &Arc, claim: Claim, new_watermark: i64, + selected_outputs: Vec, thread_id: ThreadId, phase_two_e2e_timer: Option, ) { @@ -316,7 +357,15 @@ mod agent { if let Some(token_usage) = agent_control.get_total_token_usage(thread_id).await { emit_token_usage_metrics(&session, &token_usage); } - job::succeed(&session, &db, &claim, new_watermark, "succeeded").await; + job::succeed( + &session, + &db, + &claim, + new_watermark, + &selected_outputs, + "succeeded", + ) + .await; } else { job::failed(&session, &db, &claim, "failed_agent").await; } diff --git a/codex-rs/core/src/memories/prompts.rs b/codex-rs/core/src/memories/prompts.rs index 9a3ea6a92d..35cfe1edf0 100644 --- a/codex-rs/core/src/memories/prompts.rs +++ b/codex-rs/core/src/memories/prompts.rs @@ -1,19 +1,123 @@ use crate::memories::memory_root; use crate::memories::phase_one; +use crate::memories::storage::rollout_summary_file_stem_from_parts; use crate::truncate::TruncationPolicy; use crate::truncate::truncate_text; +use askama::Template; use codex_protocol::openai_models::ModelInfo; +use codex_state::Phase2InputSelection; +use codex_state::Stage1Output; +use codex_state::Stage1OutputRef; use std::path::Path; use tokio::fs; +use tracing::warn; -const CONSOLIDATION_TEMPLATE: &str = include_str!("../../templates/memories/consolidation.md"); -const STAGE_ONE_INPUT_TEMPLATE: &str = include_str!("../../templates/memories/stage_one_input.md"); -const READ_PATH_TEMPLATE: &str = include_str!("../../templates/memories/read_path.md"); +#[derive(Template)] +#[template(path = "memories/consolidation.md", escape = "none")] +struct ConsolidationPromptTemplate<'a> { + memory_root: &'a str, + phase2_input_selection: &'a str, +} + +#[derive(Template)] +#[template(path = "memories/stage_one_input.md", escape = "none")] +struct StageOneInputTemplate<'a> { + rollout_path: &'a str, + rollout_cwd: &'a str, + rollout_contents: &'a str, +} + +#[derive(Template)] +#[template(path = "memories/read_path.md", escape = "none")] +struct MemoryToolDeveloperInstructionsTemplate<'a> { + base_path: &'a str, + memory_summary: &'a str, +} /// Builds the consolidation subagent prompt for a specific memory root. -pub(super) fn build_consolidation_prompt(memory_root: &Path) -> String { +pub(super) fn build_consolidation_prompt( + memory_root: &Path, + selection: &Phase2InputSelection, +) -> String { let memory_root = memory_root.display().to_string(); - CONSOLIDATION_TEMPLATE.replace("{{ memory_root }}", &memory_root) + let phase2_input_selection = render_phase2_input_selection(selection); + let template = ConsolidationPromptTemplate { + memory_root: &memory_root, + phase2_input_selection: &phase2_input_selection, + }; + template.render().unwrap_or_else(|err| { + warn!("failed to render memories consolidation prompt template: {err}"); + format!( + "## Memory Phase 2 (Consolidation)\nConsolidate Codex memories in: {memory_root}\n\n{phase2_input_selection}" + ) + }) +} + +fn render_phase2_input_selection(selection: &Phase2InputSelection) -> String { + let retained = selection.retained_thread_ids.len(); + let added = selection.selected.len().saturating_sub(retained); + let selected = if selection.selected.is_empty() { + "- none".to_string() + } else { + selection + .selected + .iter() + .map(|item| { + render_selected_input_line( + item, + selection.retained_thread_ids.contains(&item.thread_id), + ) + }) + .collect::>() + .join("\n") + }; + let removed = if selection.removed.is_empty() { + "- none".to_string() + } else { + selection + .removed + .iter() + .map(render_removed_input_line) + .collect::>() + .join("\n") + }; + + format!( + "- selected inputs this run: {}\n- newly added since the last successful Phase 2 run: {added}\n- retained from the last successful Phase 2 run: {retained}\n- removed from the last successful Phase 2 run: {}\n\nCurrent selected Phase 1 inputs:\n{selected}\n\nRemoved from the last successful Phase 2 selection:\n{removed}\n", + selection.selected.len(), + selection.removed.len(), + ) +} + +fn render_selected_input_line(item: &Stage1Output, retained: bool) -> String { + let status = if retained { "retained" } else { "added" }; + let rollout_summary_file = format!( + "rollout_summaries/{}.md", + rollout_summary_file_stem_from_parts( + item.thread_id, + item.source_updated_at, + item.rollout_slug.as_deref(), + ) + ); + format!( + "- [{status}] thread_id={}, rollout_summary_file={rollout_summary_file}", + item.thread_id + ) +} + +fn render_removed_input_line(item: &Stage1OutputRef) -> String { + let rollout_summary_file = format!( + "rollout_summaries/{}.md", + rollout_summary_file_stem_from_parts( + item.thread_id, + item.source_updated_at, + item.rollout_slug.as_deref(), + ) + ); + format!( + "- thread_id={}, rollout_summary_file={rollout_summary_file}", + item.thread_id + ) } /// Builds the stage-1 user message containing rollout metadata and content. @@ -40,10 +144,12 @@ pub(super) fn build_stage_one_input_message( let rollout_path = rollout_path.display().to_string(); let rollout_cwd = rollout_cwd.display().to_string(); - Ok(STAGE_ONE_INPUT_TEMPLATE - .replace("{{ rollout_path }}", &rollout_path) - .replace("{{ rollout_cwd }}", &rollout_cwd) - .replace("{{ rollout_contents }}", &truncated_rollout_contents)) + Ok(StageOneInputTemplate { + rollout_path: &rollout_path, + rollout_cwd: &rollout_cwd, + rollout_contents: &truncated_rollout_contents, + } + .render()?) } /// Build prompt used for read path. This prompt must be added to the developer instructions. In @@ -65,11 +171,11 @@ pub(crate) async fn build_memory_tool_developer_instructions(codex_home: &Path) return None; } let base_path = base_path.display().to_string(); - Some( - READ_PATH_TEMPLATE - .replace("{{ base_path }}", &base_path) - .replace("{{ memory_summary }}", &memory_summary), - ) + let template = MemoryToolDeveloperInstructionsTemplate { + base_path: &base_path, + memory_summary: &memory_summary, + }; + template.render().ok() } #[cfg(test)] diff --git a/codex-rs/core/src/memories/storage.rs b/codex-rs/core/src/memories/storage.rs index 1a07dbe9b8..1410c4e73a 100644 --- a/codex-rs/core/src/memories/storage.rs +++ b/codex-rs/core/src/memories/storage.rs @@ -82,6 +82,8 @@ async fn rebuild_raw_memories_file( ) .map_err(raw_memories_format_error)?; writeln!(body, "cwd: {}", memory.cwd.display()).map_err(raw_memories_format_error)?; + writeln!(body, "rollout_path: {}", memory.rollout_path.display()) + .map_err(raw_memories_format_error)?; let rollout_summary_file = format!("{}.md", rollout_summary_file_stem(memory)); writeln!(body, "rollout_summary_file: {rollout_summary_file}") .map_err(raw_memories_format_error)?; @@ -138,7 +140,12 @@ async fn write_rollout_summary_for_thread( memory.source_updated_at.to_rfc3339() ) .map_err(rollout_summary_format_error)?; + writeln!(body, "rollout_path: {}", memory.rollout_path.display()) + .map_err(rollout_summary_format_error)?; writeln!(body, "cwd: {}", memory.cwd.display()).map_err(rollout_summary_format_error)?; + if let Some(git_branch) = memory.git_branch.as_deref() { + writeln!(body, "git_branch: {git_branch}").map_err(rollout_summary_format_error)?; + } writeln!(body).map_err(rollout_summary_format_error)?; body.push_str(&memory.rollout_summary); body.push('\n'); @@ -267,7 +274,9 @@ mod tests { raw_memory: "raw memory".to_string(), rollout_summary: "summary".to_string(), rollout_slug: rollout_slug.map(ToString::to_string), + rollout_path: PathBuf::from("/tmp/rollout.jsonl"), cwd: PathBuf::from("/tmp/workspace"), + git_branch: None, generated_at: Utc.timestamp_opt(124, 0).single().expect("timestamp"), } } diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index b47e7f8aab..62cef7eae3 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -86,7 +86,9 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only raw_memory: "raw memory".to_string(), rollout_summary: "short summary".to_string(), rollout_slug: None, + rollout_path: PathBuf::from("/tmp/rollout-100.jsonl"), cwd: PathBuf::from("/tmp/workspace"), + git_branch: None, generated_at: Utc.timestamp_opt(101, 0).single().expect("timestamp"), }]; @@ -135,6 +137,7 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only assert!(raw_memories.contains("raw memory")); assert!(raw_memories.contains(&keep_id)); assert!(raw_memories.contains("cwd: /tmp/workspace")); + assert!(raw_memories.contains("rollout_path: /tmp/rollout-100.jsonl")); assert!(raw_memories.contains(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" ))); @@ -150,6 +153,10 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only .find("cwd: /tmp/workspace") .map(|offset| thread_pos + offset) .expect("cwd should exist after thread header"); + let rollout_path_pos = raw_memories[thread_pos..] + .find("rollout_path: /tmp/rollout-100.jsonl") + .map(|offset| thread_pos + offset) + .expect("rollout_path should exist after thread header"); let file_pos = raw_memories[thread_pos..] .find(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" @@ -158,7 +165,8 @@ async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only .expect("rollout_summary_file should exist after thread header"); assert!(thread_pos < updated_pos); assert!(updated_pos < cwd_pos); - assert!(cwd_pos < file_pos); + assert!(cwd_pos < rollout_path_pos); + assert!(rollout_path_pos < file_pos); } #[tokio::test] @@ -184,7 +192,9 @@ async fn sync_rollout_summaries_uses_timestamp_hash_and_sanitized_slug_filename( raw_memory: "raw memory".to_string(), rollout_summary: "short summary".to_string(), rollout_slug: Some("Unsafe Slug/With Spaces & Symbols + EXTRA_LONG_12345".to_string()), + rollout_path: PathBuf::from("/tmp/rollout-200.jsonl"), cwd: PathBuf::from("/tmp/workspace"), + git_branch: Some("feature/memory-branch".to_string()), generated_at: Utc.timestamp_opt(201, 0).single().expect("timestamp"), }]; @@ -239,6 +249,8 @@ async fn sync_rollout_summaries_uses_timestamp_hash_and_sanitized_slug_filename( .await .expect("read rollout summary"); assert!(summary.contains(&format!("thread_id: {thread_id}"))); + assert!(summary.contains("rollout_path: /tmp/rollout-200.jsonl")); + assert!(summary.contains("git_branch: feature/memory-branch")); assert!( !tokio::fs::try_exists(&stale_unslugged_path) .await @@ -283,7 +295,9 @@ task_outcome: success .to_string(), rollout_summary: "short summary".to_string(), rollout_slug: Some("Unsafe Slug/With Spaces & Symbols + EXTRA_LONG_12345".to_string()), + rollout_path: PathBuf::from("/tmp/rollout-200.jsonl"), cwd: PathBuf::from("/tmp/workspace"), + git_branch: None, generated_at: Utc.timestamp_opt(201, 0).single().expect("timestamp"), }]; @@ -316,6 +330,12 @@ task_outcome: success let raw_memories = tokio::fs::read_to_string(raw_memories_file(&root)) .await .expect("read raw memories"); + let summary = tokio::fs::read_to_string( + rollout_summaries_dir(&root).join(canonical_rollout_summary_file), + ) + .await + .expect("read rollout summary"); + assert!(summary.contains("rollout_path: /tmp/rollout-200.jsonl")); assert!(raw_memories.contains(&format!( "rollout_summary_file: {canonical_rollout_summary_file}" ))); @@ -360,7 +380,9 @@ mod phase2 { raw_memory: "raw memory".to_string(), rollout_summary: "rollout summary".to_string(), rollout_slug: None, + rollout_path: PathBuf::from("/tmp/rollout-summary.jsonl"), cwd: PathBuf::from("/tmp/workspace"), + git_branch: None, generated_at: chrono::DateTime::::from_timestamp(source_updated_at + 1, 0) .expect("valid generated_at timestamp"), } @@ -542,7 +564,7 @@ mod phase2 { #[tokio::test] async fn dispatch_reclaims_stale_global_lock_and_starts_consolidation() { let harness = DispatchHarness::new().await; - harness.seed_stage1_output(100).await; + harness.seed_stage1_output(Utc::now().timestamp()).await; let stale_claim = harness .state_db @@ -556,12 +578,18 @@ mod phase2 { phase2::run(&harness.session, Arc::clone(&harness.config)).await; - let running_claim = harness + let post_dispatch_claim = harness .state_db .try_claim_global_phase2_job(ThreadId::new(), 3_600) .await - .expect("claim while running"); - pretty_assertions::assert_eq!(running_claim, Phase2JobClaimOutcome::SkippedRunning); + .expect("claim after stale lock dispatch"); + assert!( + matches!( + post_dispatch_claim, + Phase2JobClaimOutcome::SkippedRunning | Phase2JobClaimOutcome::SkippedNotDirty + ), + "stale-lock dispatch should either keep the reclaimed job running or finish it before re-claim" + ); let user_input_ops = harness.user_input_ops_count(); pretty_assertions::assert_eq!(user_input_ops, 1); diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs index 7b900609b3..5c5b212403 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/core/src/models_manager/collaboration_mode_presets.rs @@ -8,9 +8,23 @@ const COLLABORATION_MODE_DEFAULT: &str = include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; +const ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER: &str = "{{ASKING_QUESTIONS_GUIDANCE}}"; -pub(crate) fn builtin_collaboration_mode_presets() -> Vec { - vec![plan_preset(), default_preset()] +/// Stores feature flags that control collaboration-mode behavior. +/// +/// Keep mode-related flags here so new collaboration-mode capabilities can be +/// added without large cross-cutting diffs to constructor and call-site +/// signatures. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CollaborationModesConfig { + /// Enables `request_user_input` availability in Default mode. + pub default_mode_request_user_input: bool, +} + +pub(crate) fn builtin_collaboration_mode_presets( + collaboration_modes_config: CollaborationModesConfig, +) -> Vec { + vec![plan_preset(), default_preset(collaboration_modes_config)] } fn plan_preset() -> CollaborationModeMask { @@ -23,26 +37,35 @@ fn plan_preset() -> CollaborationModeMask { } } -fn default_preset() -> CollaborationModeMask { +fn default_preset(collaboration_modes_config: CollaborationModesConfig) -> CollaborationModeMask { CollaborationModeMask { name: ModeKind::Default.display_name().to_string(), mode: Some(ModeKind::Default), model: None, reasoning_effort: None, - developer_instructions: Some(Some(default_mode_instructions())), + developer_instructions: Some(Some(default_mode_instructions(collaboration_modes_config))), } } -fn default_mode_instructions() -> String { +fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfig) -> String { let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); - let request_user_input_availability = - request_user_input_availability_message(ModeKind::Default); + let request_user_input_availability = request_user_input_availability_message( + ModeKind::Default, + collaboration_modes_config.default_mode_request_user_input, + ); + let asking_questions_guidance = asking_questions_guidance_message( + collaboration_modes_config.default_mode_request_user_input, + ); COLLABORATION_MODE_DEFAULT .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) .replace( REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, &request_user_input_availability, ) + .replace( + ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER, + &asking_questions_guidance, + ) } fn format_mode_names(modes: &[ModeKind]) -> String { @@ -55,9 +78,14 @@ fn format_mode_names(modes: &[ModeKind]) -> String { } } -fn request_user_input_availability_message(mode: ModeKind) -> String { +fn request_user_input_availability_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> String { let mode_name = mode.display_name(); - if mode.allows_request_user_input() { + if mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) + { format!("The `request_user_input` tool is available in {mode_name} mode.") } else { format!( @@ -66,6 +94,14 @@ fn request_user_input_availability_message(mode: ModeKind) -> String { } } +fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> String { + if default_mode_request_user_input { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() + } else { + "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string() + } +} + #[cfg(test)] mod tests { use super::*; @@ -74,7 +110,10 @@ mod tests { #[test] fn preset_names_use_mode_display_names() { assert_eq!(plan_preset().name, ModeKind::Plan.display_name()); - assert_eq!(default_preset().name, ModeKind::Default.display_name()); + assert_eq!( + default_preset(CollaborationModesConfig::default()).name, + ModeKind::Default.display_name() + ); assert_eq!( plan_preset().reasoning_effort, Some(Some(ReasoningEffort::Medium)) @@ -83,20 +122,38 @@ mod tests { #[test] fn default_mode_instructions_replace_mode_names_placeholder() { - let default_instructions = default_preset() - .developer_instructions - .expect("default preset should include instructions") - .expect("default instructions should be set"); + let default_instructions = default_preset(CollaborationModesConfig { + default_mode_request_user_input: true, + }) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER)); assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER)); + assert!(!default_instructions.contains(ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER)); let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); let expected_snippet = format!("Known mode names are {known_mode_names}."); assert!(default_instructions.contains(&expected_snippet)); let expected_availability_message = - request_user_input_availability_message(ModeKind::Default); + request_user_input_availability_message(ModeKind::Default, true); assert!(default_instructions.contains(&expected_availability_message)); + assert!(default_instructions.contains("prefer using the `request_user_input` tool")); + } + + #[test] + fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() { + let default_instructions = default_preset(CollaborationModesConfig::default()) + .developer_instructions + .expect("default preset should include instructions") + .expect("default instructions should be set"); + + assert!(!default_instructions.contains("prefer using the `request_user_input` tool")); + assert!( + default_instructions + .contains("ask the user directly with a concise plain-text question") + ); } } diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 4b1da3e58a..1d7309020e 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -8,6 +8,7 @@ use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result as CoreResult; use crate::model_provider_info::ModelProviderInfo; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; use crate::models_manager::model_info; use codex_api::ModelsClient; @@ -41,11 +42,21 @@ pub enum RefreshStrategy { OnlineIfUncached, } +/// How the manager's base catalog is sourced for the lifetime of the process. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CatalogMode { + /// Start from bundled `models.json` and allow cache/network refresh updates. + Default, + /// Use a caller-provided catalog as authoritative and do not mutate it via refresh. + Custom, +} + /// Coordinates remote model discovery plus cached metadata on disk. #[derive(Debug)] pub struct ModelsManager { remote_models: RwLock>, - has_custom_model_catalog: bool, + catalog_mode: CatalogMode, + collaboration_modes_config: CollaborationModesConfig, auth_manager: Arc, etag: RwLock>, cache_manager: ModelsCacheManager, @@ -62,10 +73,15 @@ impl ModelsManager { codex_home: PathBuf, auth_manager: Arc, model_catalog: Option, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let cache_path = codex_home.join(MODEL_CACHE_FILE); let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL); - let has_custom_model_catalog = model_catalog.is_some(); + let catalog_mode = if model_catalog.is_some() { + CatalogMode::Custom + } else { + CatalogMode::Default + }; let remote_models = model_catalog .map(|catalog| catalog.models) .unwrap_or_else(|| { @@ -74,7 +90,8 @@ impl ModelsManager { }); Self { remote_models: RwLock::new(remote_models), - has_custom_model_catalog, + catalog_mode, + collaboration_modes_config, auth_manager, etag: RwLock::new(None), cache_manager, @@ -97,7 +114,14 @@ impl ModelsManager { /// /// Returns a static set of presets seeded with the configured model. pub fn list_collaboration_modes(&self) -> Vec { - builtin_collaboration_mode_presets() + self.list_collaboration_modes_for_config(self.collaboration_modes_config) + } + + pub fn list_collaboration_modes_for_config( + &self, + collaboration_modes_config: CollaborationModesConfig, + ) -> Vec { + builtin_collaboration_mode_presets(collaboration_modes_config) } /// Attempt to list models without blocking, using the current cached state. @@ -217,7 +241,7 @@ impl ModelsManager { /// Refresh available models according to the specified strategy. async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()> { // don't override the custom model catalog if one was provided by the user - if self.has_custom_model_catalog { + if matches!(self.catalog_mode, CatalogMode::Custom) { return Ok(()); } @@ -364,7 +388,8 @@ impl ModelsManager { Self::load_remote_models_from_file() .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")), ), - has_custom_model_catalog: false, + catalog_mode: CatalogMode::Default, + collaboration_modes_config: CollaborationModesConfig::default(), auth_manager, etag: RwLock::new(None), cache_manager, @@ -491,7 +516,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -528,6 +558,7 @@ mod tests { Some(ModelsResponse { models: vec![remote_model("gpt-overlay", "Overlay", 0)], }), + CollaborationModesConfig::default(), ); let model_info = manager @@ -551,7 +582,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await @@ -577,7 +613,12 @@ mod tests { .expect("load default test config"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let manager = ModelsManager::new(codex_home.path().to_path_buf(), auth_manager, None); + let manager = ModelsManager::new( + codex_home.path().to_path_buf(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let known_slug = manager .get_remote_models() .await diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 19a945f951..4824e4cd11 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -1,3 +1,4 @@ +use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInstructionsVariables; @@ -68,10 +69,12 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { visibility: ModelVisibility::None, supported_in_api: true, priority: 99, + availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), model_messages: local_personality_messages_for_slug(slug), supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, diff --git a/codex-rs/core/src/proposed_plan_parser.rs b/codex-rs/core/src/proposed_plan_parser.rs deleted file mode 100644 index 44be264f29..0000000000 --- a/codex-rs/core/src/proposed_plan_parser.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::tagged_block_parser::TagSpec; -use crate::tagged_block_parser::TaggedLineParser; -use crate::tagged_block_parser::TaggedLineSegment; - -const OPEN_TAG: &str = ""; -const CLOSE_TAG: &str = ""; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum PlanTag { - ProposedPlan, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ProposedPlanSegment { - Normal(String), - ProposedPlanStart, - ProposedPlanDelta(String), - ProposedPlanEnd, -} - -/// Parser for `` blocks emitted in plan mode. -/// -/// This is a thin wrapper around the generic line-based tag parser. It maps -/// tag-aware segments into plan-specific segments for downstream consumers. -#[derive(Debug)] -pub(crate) struct ProposedPlanParser { - parser: TaggedLineParser, -} - -impl ProposedPlanParser { - pub(crate) fn new() -> Self { - Self { - parser: TaggedLineParser::new(vec![TagSpec { - open: OPEN_TAG, - close: CLOSE_TAG, - tag: PlanTag::ProposedPlan, - }]), - } - } - - pub(crate) fn parse(&mut self, delta: &str) -> Vec { - self.parser - .parse(delta) - .into_iter() - .map(map_plan_segment) - .collect() - } - - pub(crate) fn finish(&mut self) -> Vec { - self.parser - .finish() - .into_iter() - .map(map_plan_segment) - .collect() - } -} - -fn map_plan_segment(segment: TaggedLineSegment) -> ProposedPlanSegment { - match segment { - TaggedLineSegment::Normal(text) => ProposedPlanSegment::Normal(text), - TaggedLineSegment::TagStart(PlanTag::ProposedPlan) => { - ProposedPlanSegment::ProposedPlanStart - } - TaggedLineSegment::TagDelta(PlanTag::ProposedPlan, text) => { - ProposedPlanSegment::ProposedPlanDelta(text) - } - TaggedLineSegment::TagEnd(PlanTag::ProposedPlan) => ProposedPlanSegment::ProposedPlanEnd, - } -} - -pub(crate) fn strip_proposed_plan_blocks(text: &str) -> String { - let mut parser = ProposedPlanParser::new(); - let mut out = String::new(); - for segment in parser.parse(text).into_iter().chain(parser.finish()) { - if let ProposedPlanSegment::Normal(delta) = segment { - out.push_str(&delta); - } - } - out -} - -pub(crate) fn extract_proposed_plan_text(text: &str) -> Option { - let mut parser = ProposedPlanParser::new(); - let mut plan_text = String::new(); - let mut saw_plan_block = false; - for segment in parser.parse(text).into_iter().chain(parser.finish()) { - match segment { - ProposedPlanSegment::ProposedPlanStart => { - saw_plan_block = true; - plan_text.clear(); - } - ProposedPlanSegment::ProposedPlanDelta(delta) => { - plan_text.push_str(&delta); - } - ProposedPlanSegment::ProposedPlanEnd | ProposedPlanSegment::Normal(_) => {} - } - } - saw_plan_block.then_some(plan_text) -} - -#[cfg(test)] -mod tests { - use super::ProposedPlanParser; - use super::ProposedPlanSegment; - use super::strip_proposed_plan_blocks; - use pretty_assertions::assert_eq; - - #[test] - fn streams_proposed_plan_segments() { - let mut parser = ProposedPlanParser::new(); - let mut segments = Vec::new(); - - for chunk in [ - "Intro text\n\n- step 1\n", - "\nOutro", - ] { - segments.extend(parser.parse(chunk)); - } - segments.extend(parser.finish()); - - assert_eq!( - segments, - vec![ - ProposedPlanSegment::Normal("Intro text\n".to_string()), - ProposedPlanSegment::ProposedPlanStart, - ProposedPlanSegment::ProposedPlanDelta("- step 1\n".to_string()), - ProposedPlanSegment::ProposedPlanEnd, - ProposedPlanSegment::Normal("Outro".to_string()), - ] - ); - } - - #[test] - fn preserves_non_tag_lines() { - let mut parser = ProposedPlanParser::new(); - let mut segments = parser.parse(" extra\n"); - segments.extend(parser.finish()); - - assert_eq!( - segments, - vec![ProposedPlanSegment::Normal( - " extra\n".to_string() - )] - ); - } - - #[test] - fn closes_unterminated_plan_block_on_finish() { - let mut parser = ProposedPlanParser::new(); - let mut segments = parser.parse("\n- step 1\n"); - segments.extend(parser.finish()); - - assert_eq!( - segments, - vec![ - ProposedPlanSegment::ProposedPlanStart, - ProposedPlanSegment::ProposedPlanDelta("- step 1\n".to_string()), - ProposedPlanSegment::ProposedPlanEnd, - ] - ); - } - - #[test] - fn closes_tag_line_without_trailing_newline() { - let mut parser = ProposedPlanParser::new(); - let mut segments = parser.parse("\n- step 1\n"); - segments.extend(parser.finish()); - - assert_eq!( - segments, - vec![ - ProposedPlanSegment::ProposedPlanStart, - ProposedPlanSegment::ProposedPlanDelta("- step 1\n".to_string()), - ProposedPlanSegment::ProposedPlanEnd, - ] - ); - } - - #[test] - fn strips_proposed_plan_blocks_from_text() { - let text = "before\n\n- step\n\nafter"; - assert_eq!(strip_proposed_plan_blocks(text), "before\nafter"); - } -} diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 47a4354e2d..d643d74682 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -29,7 +29,9 @@ use serde_json::Value; use std::sync::Arc; use tokio::sync::Mutex; use tokio::task::JoinHandle; +use tracing::debug; use tracing::error; +use tracing::info; use tracing::warn; const AUDIO_IN_QUEUE_CAPACITY: usize = 256; @@ -183,6 +185,7 @@ pub(crate) async fn handle_start( let requested_session_id = params .session_id .or_else(|| Some(sess.conversation_id.to_string())); + info!("starting realtime conversation"); let events_rx = match sess .conversation .start(api_provider, None, prompt, requested_session_id.clone()) @@ -190,11 +193,14 @@ pub(crate) async fn handle_start( { Ok(events_rx) => events_rx, Err(err) => { + error!("failed to start realtime conversation: {err}"); send_conversation_error(sess, sub_id, err.to_string(), CodexErrorInfo::Other).await; return Ok(()); } }; + info!("realtime conversation started"); + sess.send_event_raw(Event { id: sub_id.clone(), msg: EventMsg::RealtimeConversationStarted(RealtimeConversationStartedEvent { @@ -210,6 +216,7 @@ pub(crate) async fn handle_start( msg, }; while let Ok(event) = events_rx.recv().await { + debug!(conversation_id = %sess_clone.conversation_id, "received realtime conversation event"); let maybe_routed_text = match &event { RealtimeEvent::ConversationItemAdded(item) => { realtime_text_from_conversation_item(item) @@ -217,6 +224,7 @@ pub(crate) async fn handle_start( _ => None, }; if let Some(text) = maybe_routed_text { + debug!(text = %text, "[realtime-text] realtime conversation text output"); let sess_for_routed_text = Arc::clone(&sess_clone); sess_for_routed_text.route_realtime_text_input(text).await; } @@ -229,6 +237,7 @@ pub(crate) async fn handle_start( .await; } if let Some(()) = sess_clone.conversation.running_state().await { + info!("realtime conversation transport closed"); sess_clone .send_event_raw(ev(EventMsg::RealtimeConversationClosed( RealtimeConversationClosedEvent { @@ -248,6 +257,7 @@ pub(crate) async fn handle_audio( params: ConversationAudioParams, ) { if let Err(err) = sess.conversation.audio_in(params.frame).await { + error!("failed to append realtime audio: {err}"); send_conversation_error(sess, sub_id, err.to_string(), CodexErrorInfo::BadRequest).await; } } @@ -279,7 +289,10 @@ pub(crate) async fn handle_text( sub_id: String, params: ConversationTextParams, ) { + debug!(text = %params.text, "[realtime-text] appending realtime conversation text input"); + if let Err(err) = sess.conversation.text_in(params.text).await { + error!("failed to append realtime text: {err}"); send_conversation_error(sess, sub_id, err.to_string(), CodexErrorInfo::BadRequest).await; } } diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 8232330232..4c76bc54f6 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -120,7 +120,9 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::CollabAgentInteractionEnd(_) | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseEnd(_) - | EventMsg::CollabResumeEnd(_) => Some(EventPersistenceMode::Extended), + | EventMsg::CollabResumeEnd(_) + | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) => Some(EventPersistenceMode::Extended), EventMsg::Warning(_) | EventMsg::RealtimeConversationStarted(_) | EventMsg::RealtimeConversationRealtime(_) @@ -139,9 +141,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::TerminalInteraction(_) | EventMsg::ExecCommandOutputDelta(_) | EventMsg::ExecApprovalRequest(_) - | EventMsg::SkillRequestApproval(_) | EventMsg::RequestUserInput(_) - | EventMsg::DynamicToolCallRequest(_) | EventMsg::ElicitationRequest(_) | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::BackgroundEvent(_) diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 7e639e382e..d7404fd2c6 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -160,6 +160,7 @@ fn sanitize_rollout_item_for_persistence( impl RolloutRecorder { /// List threads (rollout files) under the provided Codex home directory. + #[allow(clippy::too_many_arguments)] pub async fn list_threads( config: &Config, page_size: usize, @@ -168,6 +169,7 @@ impl RolloutRecorder { allowed_sources: &[SessionSource], model_providers: Option<&[String]>, default_provider: &str, + search_term: Option<&str>, ) -> std::io::Result { Self::list_threads_with_db_fallback( config, @@ -178,11 +180,13 @@ impl RolloutRecorder { model_providers, default_provider, false, + search_term, ) .await } /// List archived threads (rollout files) under the archived sessions directory. + #[allow(clippy::too_many_arguments)] pub async fn list_archived_threads( config: &Config, page_size: usize, @@ -191,6 +195,7 @@ impl RolloutRecorder { allowed_sources: &[SessionSource], model_providers: Option<&[String]>, default_provider: &str, + search_term: Option<&str>, ) -> std::io::Result { Self::list_threads_with_db_fallback( config, @@ -201,6 +206,7 @@ impl RolloutRecorder { model_providers, default_provider, true, + search_term, ) .await } @@ -215,6 +221,7 @@ impl RolloutRecorder { model_providers: Option<&[String]>, default_provider: &str, archived: bool, + search_term: Option<&str>, ) -> std::io::Result { let codex_home = config.codex_home.as_path(); // Filesystem-first listing intentionally overfetches so we can repair stale/missing @@ -275,6 +282,7 @@ impl RolloutRecorder { allowed_sources, model_providers, archived, + search_term, ) .await { @@ -312,12 +320,15 @@ impl RolloutRecorder { allowed_sources, model_providers, false, + None, ) .await else { break; }; - if let Some(path) = select_resume_path_from_db_page(&db_page, filter_cwd) { + if let Some(path) = + select_resume_path_from_db_page(&db_page, filter_cwd, default_provider).await + { return Ok(Some(path)); } db_cursor = db_page.next_anchor.map(Into::into); @@ -339,7 +350,7 @@ impl RolloutRecorder { default_provider, ) .await?; - if let Some(path) = select_resume_path(&page, filter_cwd) { + if let Some(path) = select_resume_path(&page, filter_cwd, default_provider).await { return Ok(Some(path)); } cursor = page.next_cursor; @@ -449,6 +460,7 @@ impl RolloutRecorder { state_db_ctx.clone(), state_builder, config.model_provider_id.clone(), + config.memories.generate_memories, )); Ok(Self { @@ -700,6 +712,7 @@ async fn rollout_writer( state_db_ctx: Option, mut state_builder: Option, default_provider: String, + generate_memories: bool, ) -> std::io::Result<()> { let mut writer = file.map(|file| JsonlWriter { file }); let mut buffered_items = Vec::::new(); @@ -720,6 +733,7 @@ async fn rollout_writer( state_db_ctx.as_deref(), &mut state_builder, default_provider.as_str(), + generate_memories, ) .await?; } @@ -773,6 +787,7 @@ async fn rollout_writer( state_db_ctx.as_deref(), &mut state_builder, default_provider.as_str(), + generate_memories, ) .await?; } @@ -820,6 +835,7 @@ async fn rollout_writer( Ok(()) } +#[allow(clippy::too_many_arguments)] async fn write_session_meta( mut writer: Option<&mut JsonlWriter>, session_meta: SessionMeta, @@ -828,6 +844,7 @@ async fn write_session_meta( state_db_ctx: Option<&StateRuntime>, state_builder: &mut Option, default_provider: &str, + generate_memories: bool, ) -> std::io::Result<()> { let git_info = collect_git_info(cwd).await; let session_meta_line = SessionMetaLine { @@ -849,6 +866,7 @@ async fn write_session_meta( state_builder.as_ref(), std::slice::from_ref(&rollout_item), None, + (!generate_memories).then_some("disabled"), ) .await; Ok(()) @@ -877,6 +895,7 @@ async fn write_and_reconcile_items( state_builder.as_ref(), items, "rollout_writer", + None, ) .await; Ok(()) @@ -952,35 +971,79 @@ impl From for ThreadsPage { } } -fn select_resume_path(page: &ThreadsPage, filter_cwd: Option<&Path>) -> Option { +async fn select_resume_path( + page: &ThreadsPage, + filter_cwd: Option<&Path>, + default_provider: &str, +) -> Option { match filter_cwd { - Some(cwd) => page.items.iter().find_map(|item| { - if item - .cwd - .as_ref() - .is_some_and(|session_cwd| cwd_matches(session_cwd, cwd)) - { - Some(item.path.clone()) - } else { - None + Some(cwd) => { + for item in &page.items { + if resume_candidate_matches_cwd( + item.path.as_path(), + item.cwd.as_deref(), + cwd, + default_provider, + ) + .await + { + return Some(item.path.clone()); + } } - }), + None + } None => page.items.first().map(|item| item.path.clone()), } } -fn select_resume_path_from_db_page( +async fn resume_candidate_matches_cwd( + rollout_path: &Path, + cached_cwd: Option<&Path>, + cwd: &Path, + default_provider: &str, +) -> bool { + if cached_cwd.is_some_and(|session_cwd| cwd_matches(session_cwd, cwd)) { + return true; + } + + if let Ok((items, _, _)) = RolloutRecorder::load_rollout_items(rollout_path).await + && let Some(latest_turn_context_cwd) = items.iter().rev().find_map(|item| match item { + RolloutItem::TurnContext(turn_context) => Some(turn_context.cwd.as_path()), + RolloutItem::SessionMeta(_) + | RolloutItem::ResponseItem(_) + | RolloutItem::Compacted(_) + | RolloutItem::EventMsg(_) => None, + }) + { + return cwd_matches(latest_turn_context_cwd, cwd); + } + + metadata::extract_metadata_from_rollout(rollout_path, default_provider, None) + .await + .is_ok_and(|outcome| cwd_matches(outcome.metadata.cwd.as_path(), cwd)) +} + +async fn select_resume_path_from_db_page( page: &codex_state::ThreadsPage, filter_cwd: Option<&Path>, + default_provider: &str, ) -> Option { match filter_cwd { - Some(cwd) => page.items.iter().find_map(|item| { - if cwd_matches(item.cwd.as_path(), cwd) { - Some(item.rollout_path.clone()) - } else { - None + Some(cwd) => { + for item in &page.items { + if resume_candidate_matches_cwd( + item.rollout_path.as_path(), + Some(item.cwd.as_path()), + cwd, + default_provider, + ) + .await + { + return Some(item.rollout_path.clone()); + } } - }), + None + } None => page.items.first().map(|item| item.rollout_path.clone()), } } @@ -1001,8 +1064,12 @@ mod tests { use crate::config::ConfigBuilder; use crate::features::Feature; use chrono::TimeZone; + use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::protocol::AgentMessageEvent; + use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; + use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::UserMessageEvent; use pretty_assertions::assert_eq; use std::fs::File; @@ -1154,6 +1221,7 @@ mod tests { &[], None, default_provider.as_str(), + None, ) .await?; assert_eq!(page1.items.len(), 1); @@ -1168,6 +1236,7 @@ mod tests { &[], None, default_provider.as_str(), + None, ) .await?; assert_eq!(page2.items.len(), 1); @@ -1229,6 +1298,7 @@ mod tests { &[], None, default_provider.as_str(), + None, ) .await?; assert_eq!(page.items.len(), 0); @@ -1295,6 +1365,7 @@ mod tests { &[], None, default_provider.as_str(), + None, ) .await?; assert_eq!(page.items.len(), 1); @@ -1307,4 +1378,49 @@ mod tests { assert_eq!(repaired_path, Some(real_path)); Ok(()) } + + #[tokio::test] + async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Result<()> { + let home = TempDir::new().expect("temp dir"); + let stale_cwd = home.path().join("stale"); + let latest_cwd = home.path().join("latest"); + fs::create_dir_all(&stale_cwd)?; + fs::create_dir_all(&latest_cwd)?; + + let path = write_session_file(home.path(), "2025-01-03T13-00-00", Uuid::from_u128(9012))?; + let mut file = std::fs::OpenOptions::new().append(true).open(&path)?; + let turn_context = RolloutLine { + timestamp: "2025-01-03T13:00:01Z".to_string(), + item: RolloutItem::TurnContext(TurnContextItem { + turn_id: Some("turn-1".to_string()), + cwd: latest_cwd.clone(), + current_date: None, + timezone: None, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + network: None, + model: "test-model".to_string(), + personality: None, + collaboration_mode: None, + effort: None, + summary: ReasoningSummaryConfig::Auto, + user_instructions: None, + developer_instructions: None, + final_output_json_schema: None, + truncation_policy: None, + }), + }; + writeln!(file, "{}", serde_json::to_string(&turn_context)?)?; + + assert!( + resume_candidate_matches_cwd( + path.as_path(), + Some(stale_cwd.as_path()), + latest_cwd.as_path(), + "test-provider", + ) + .await + ); + Ok(()) + } } diff --git a/codex-rs/core/src/rollout/truncation.rs b/codex-rs/core/src/rollout/truncation.rs index c50eacc48b..7b316c0bb5 100644 --- a/codex-rs/core/src/rollout/truncation.rs +++ b/codex-rs/core/src/rollout/truncation.rs @@ -193,7 +193,7 @@ mod tests { #[tokio::test] async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() { let (session, turn_context) = make_session_and_context().await; - let mut items = session.build_initial_context(&turn_context).await; + let mut items = session.build_initial_context(&turn_context, None).await; items.push(user_msg("feature request")); items.push(assistant_msg("ack")); items.push(user_msg("second question")); diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 70ca5588e0..bbd73749e4 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -17,14 +17,17 @@ use crate::protocol::SandboxPolicy; #[cfg(target_os = "macos")] use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; #[cfg(target_os = "macos")] -use crate::seatbelt::create_seatbelt_command_args; +use crate::seatbelt::create_seatbelt_command_args_with_extensions; #[cfg(target_os = "macos")] use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::FileSystemPermissions; +#[cfg(target_os = "macos")] +use codex_protocol::models::MacOsSeatbeltProfileExtensions; +use codex_protocol::models::PermissionProfile; pub use codex_protocol::models::SandboxPermissions; use codex_protocol::protocol::ReadOnlyAccess; use codex_utils_absolute_path::AbsolutePathBuf; @@ -41,7 +44,7 @@ pub struct CommandSpec { pub env: HashMap, pub expiration: ExecExpiration, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub justification: Option, } @@ -72,6 +75,8 @@ pub(crate) struct SandboxTransformRequest<'a> { // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a Path, + #[cfg(target_os = "macos")] + pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>, pub codex_linux_sandbox_exe: Option<&'a PathBuf>, pub use_linux_sandbox_bwrap: bool, pub windows_sandbox_level: WindowsSandboxLevel, @@ -87,67 +92,49 @@ pub enum SandboxPreference { pub(crate) enum SandboxTransformError { #[error("missing codex-linux-sandbox executable path")] MissingLinuxSandboxExecutable, - #[error("invalid additional permissions path: {0}")] - InvalidAdditionalPermissionsPath(String), #[cfg(not(target_os = "macos"))] #[error("seatbelt sandbox is only available on macOS")] SeatbeltUnavailable, } pub(crate) fn normalize_additional_permissions( - additional_permissions: AdditionalPermissions, - command_cwd: &Path, -) -> Result { - let fs_read = - normalize_permission_paths(additional_permissions.fs_read, command_cwd, "fs_read")?; - let fs_write = - normalize_permission_paths(additional_permissions.fs_write, command_cwd, "fs_write")?; - Ok(AdditionalPermissions { fs_read, fs_write }) + additional_permissions: PermissionProfile, +) -> Result { + let Some(file_system) = additional_permissions.file_system else { + return Ok(PermissionProfile::default()); + }; + let read = file_system + .read + .map(|paths| normalize_permission_paths(paths, "file_system.read")); + let write = file_system + .write + .map(|paths| normalize_permission_paths(paths, "file_system.write")); + Ok(PermissionProfile { + file_system: Some(FileSystemPermissions { read, write }), + ..Default::default() + }) } fn normalize_permission_paths( - paths: Vec, - command_cwd: &Path, - permission_kind: &str, -) -> Result, String> { + paths: Vec, + _permission_kind: &str, +) -> Vec { let mut out = Vec::with_capacity(paths.len()); let mut seen = HashSet::new(); for path in paths { - if path.as_os_str().is_empty() { - return Err(format!("{permission_kind} contains an empty path")); - } - - let resolved = if path.is_absolute() { - AbsolutePathBuf::from_absolute_path(path.clone()).map_err(|err| { - format!( - "{permission_kind} path `{}` is invalid: {err}", - path.display() - ) - })? - } else { - AbsolutePathBuf::resolve_path_against_base(&path, command_cwd).map_err(|err| { - format!( - "{permission_kind} path `{}` cannot be resolved against cwd `{}`: {err}", - path.display(), - command_cwd.display() - ) - })? - }; - - let canonicalized = resolved + let canonicalized = path .as_path() .canonicalize() .ok() .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) - .unwrap_or(resolved); - let canonicalized = canonicalized.to_path_buf(); + .unwrap_or(path); if seen.insert(canonicalized.clone()) { out.push(canonicalized); } } - Ok(out) + out } fn dedup_absolute_paths(paths: Vec) -> Vec { @@ -162,26 +149,24 @@ fn dedup_absolute_paths(paths: Vec) -> Vec { } fn additional_permission_roots( - additional_permissions: &AdditionalPermissions, -) -> Result<(Vec, Vec), SandboxTransformError> { - let to_abs = |paths: &[PathBuf]| { - let mut out = Vec::with_capacity(paths.len()); - for path in paths { - let absolute = AbsolutePathBuf::from_absolute_path(path.clone()).map_err(|err| { - SandboxTransformError::InvalidAdditionalPermissionsPath(format!( - "`{}`: {err}", - path.display() - )) - })?; - out.push(absolute); - } - Ok(dedup_absolute_paths(out)) - }; - - Ok(( - to_abs(&additional_permissions.fs_read)?, - to_abs(&additional_permissions.fs_write)?, - )) + additional_permissions: &PermissionProfile, +) -> (Vec, Vec) { + ( + dedup_absolute_paths( + additional_permissions + .file_system + .as_ref() + .and_then(|file_system| file_system.read.clone()) + .unwrap_or_default(), + ), + dedup_absolute_paths( + additional_permissions + .file_system + .as_ref() + .and_then(|file_system| file_system.write.clone()) + .unwrap_or_default(), + ), + ) } fn merge_read_only_access_with_additional_reads( @@ -206,13 +191,13 @@ fn merge_read_only_access_with_additional_reads( fn sandbox_policy_with_additional_permissions( sandbox_policy: &SandboxPolicy, - additional_permissions: &AdditionalPermissions, + additional_permissions: &PermissionProfile, ) -> Result { if additional_permissions.is_empty() { return Ok(sandbox_policy.clone()); } - let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions)?; + let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions); let policy = match sandbox_policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { @@ -319,6 +304,8 @@ impl SandboxManager { enforce_managed_network, network, sandbox_policy_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions, codex_linux_sandbox_exe, use_linux_sandbox_bwrap, windows_sandbox_level, @@ -347,12 +334,13 @@ impl SandboxManager { SandboxType::MacosSeatbelt => { let mut seatbelt_env = HashMap::new(); seatbelt_env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string()); - let mut args = create_seatbelt_command_args( + let mut args = create_seatbelt_command_args_with_extensions( command.clone(), &effective_policy, sandbox_policy_cwd, enforce_managed_network, network, + macos_seatbelt_profile_extensions, ); let mut full_command = Vec::with_capacity(1 + args.len()); full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()); diff --git a/codex-rs/core/src/seatbelt_permissions.rs b/codex-rs/core/src/seatbelt_permissions.rs index aacc3a7784..6b9fa68188 100644 --- a/codex-rs/core/src/seatbelt_permissions.rs +++ b/codex-rs/core/src/seatbelt_permissions.rs @@ -3,34 +3,9 @@ use std::collections::BTreeSet; use std::path::PathBuf; -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum MacOsPreferencesPermission { - // IMPORTANT: ReadOnly needs to be the default because it's the security-sensitive default. - // it's important for allowing cf prefs to work. - #[default] - ReadOnly, - ReadWrite, - None, -} - -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum MacOsAutomationPermission { - #[default] - None, - All, - BundleIds(Vec), -} - -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct MacOsSeatbeltProfileExtensions { - pub macos_preferences: MacOsPreferencesPermission, - pub macos_automation: MacOsAutomationPermission, - pub macos_accessibility: bool, - pub macos_calendar: bool, -} +pub use codex_protocol::models::MacOsAutomationPermission; +pub use codex_protocol::models::MacOsPreferencesPermission; +pub use codex_protocol::models::MacOsSeatbeltProfileExtensions; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) struct SeatbeltExtensionPolicy { @@ -38,25 +13,26 @@ pub(crate) struct SeatbeltExtensionPolicy { pub(crate) dir_params: Vec<(String, PathBuf)>, } -impl MacOsSeatbeltProfileExtensions { - pub fn normalized(&self) -> Self { - let mut normalized = self.clone(); - if let MacOsAutomationPermission::BundleIds(bundle_ids) = &self.macos_automation { - let bundle_ids = normalize_bundle_ids(bundle_ids); - normalized.macos_automation = if bundle_ids.is_empty() { - MacOsAutomationPermission::None - } else { - MacOsAutomationPermission::BundleIds(bundle_ids) - }; - } - normalized +fn normalized_extensions( + extensions: &MacOsSeatbeltProfileExtensions, +) -> MacOsSeatbeltProfileExtensions { + let mut normalized = extensions.clone(); + if let MacOsAutomationPermission::BundleIds(bundle_ids) = &extensions.macos_automation { + let bundle_ids = normalize_bundle_ids(bundle_ids); + normalized.macos_automation = if bundle_ids.is_empty() { + MacOsAutomationPermission::None + } else { + MacOsAutomationPermission::BundleIds(bundle_ids) + }; } + + normalized } pub(crate) fn build_seatbelt_extensions( extensions: &MacOsSeatbeltProfileExtensions, ) -> SeatbeltExtensionPolicy { - let extensions = extensions.normalized(); + let extensions = normalized_extensions(extensions); let mut clauses = Vec::new(); match extensions.macos_preferences { diff --git a/codex-rs/core/src/session_prefix.rs b/codex-rs/core/src/session_prefix.rs index 5f8516ba93..db3ac00a6d 100644 --- a/codex-rs/core/src/session_prefix.rs +++ b/codex-rs/core/src/session_prefix.rs @@ -1,28 +1,8 @@ use codex_protocol::protocol::AgentStatus; -/// Helpers for identifying model-visible "session prefix" messages. -/// -/// A session prefix is a user-role message that carries configuration or state needed by -/// follow-up turns (e.g. ``, ``). These items are persisted in -/// history so the model can see them, but they are not user intent and must not create user-turn -/// boundaries. -pub(crate) const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = ""; -pub(crate) const TURN_ABORTED_OPEN_TAG: &str = ""; -pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = ""; -pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = ""; - -fn starts_with_ascii_case_insensitive(text: &str, prefix: &str) -> bool { - text.get(..prefix.len()) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix)) -} - -/// Returns true if `text` starts with a session prefix marker (case-insensitive). -pub(crate) fn is_session_prefix(text: &str) -> bool { - let trimmed = text.trim_start(); - starts_with_ascii_case_insensitive(trimmed, ENVIRONMENT_CONTEXT_OPEN_TAG) - || starts_with_ascii_case_insensitive(trimmed, TURN_ABORTED_OPEN_TAG) - || starts_with_ascii_case_insensitive(trimmed, SUBAGENT_NOTIFICATION_OPEN_TAG) -} +/// Helpers for model-visible session state markers that are stored in user-role +/// messages but are not user intent. +use crate::contextual_user_message::SUBAGENT_NOTIFICATION_FRAGMENT; pub(crate) fn format_subagent_notification_message(agent_id: &str, status: &AgentStatus) -> String { let payload_json = serde_json::json!({ @@ -30,19 +10,12 @@ pub(crate) fn format_subagent_notification_message(agent_id: &str, status: &Agen "status": status, }) .to_string(); - format!("{SUBAGENT_NOTIFICATION_OPEN_TAG}\n{payload_json}\n{SUBAGENT_NOTIFICATION_CLOSE_TAG}") + SUBAGENT_NOTIFICATION_FRAGMENT.wrap(payload_json) } -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn is_session_prefix_is_case_insensitive() { - assert_eq!( - is_session_prefix("{}"), - true - ); +pub(crate) fn format_subagent_context_line(agent_id: &str, agent_nickname: Option<&str>) -> String { + match agent_nickname.filter(|nickname| !nickname.is_empty()) { + Some(agent_nickname) => format!("- {agent_id}: {agent_nickname}"), + None => format!("- {agent_id}"), } } diff --git a/codex-rs/core/src/skills/injection.rs b/codex-rs/core/src/skills/injection.rs index 22767d6dc1..e7856897a2 100644 --- a/codex-rs/core/src/skills/injection.rs +++ b/codex-rs/core/src/skills/injection.rs @@ -482,6 +482,7 @@ mod tests { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: PathBuf::from(path), scope: codex_protocol::protocol::SkillScope::User, diff --git a/codex-rs/core/src/skills/invocation_utils.rs b/codex-rs/core/src/skills/invocation_utils.rs index 65f077f96a..d2831c4dfb 100644 --- a/codex-rs/core/src/skills/invocation_utils.rs +++ b/codex-rs/core/src/skills/invocation_utils.rs @@ -7,21 +7,8 @@ use crate::analytics_client::SkillInvocation; use crate::analytics_client::build_track_events_context; use crate::codex::Session; use crate::codex::TurnContext; -use crate::features::Feature; use crate::skills::SkillLoadOutcome; use crate::skills::SkillMetadata; -use codex_protocol::protocol::ReviewDecision; -use serde::Serialize; - -pub(crate) const SKILL_APPROVAL_DECLINED_MESSAGE: &str = - "This script is part of the skill and the user declined the skill usage"; - -#[derive(Debug, Serialize)] -struct SkillApprovalCacheKey { - skill_name: String, - skill_path: PathBuf, - skill_scope: codex_protocol::protocol::SkillScope, -} pub(crate) fn build_implicit_skill_path_indexes( skills: Vec, @@ -54,11 +41,8 @@ fn detect_implicit_skill_invocation_for_command( let workdir = normalize_path(workdir.as_path()); let tokens = tokenize_command(command); - if let Some(candidate) = detect_implicit_skill_script_invocation_for_tokens( - outcome, - tokens.as_slice(), - workdir.as_path(), - ) { + if let Some(candidate) = detect_skill_script_run(outcome, tokens.as_slice(), workdir.as_path()) + { return Some(candidate); } @@ -69,82 +53,6 @@ fn detect_implicit_skill_invocation_for_command( None } -pub(crate) fn detect_implicit_skill_script_invocation_for_command( - outcome: &SkillLoadOutcome, - command: &str, - workdir: &Path, -) -> Option { - let tokens = tokenize_command(command); - - detect_implicit_skill_script_invocation_for_tokens(outcome, tokens.as_slice(), workdir) -} - -pub(crate) fn detect_implicit_skill_script_invocation_for_tokens( - outcome: &SkillLoadOutcome, - command: &[String], - workdir: &Path, -) -> Option { - detect_skill_script_run(outcome, command, workdir) -} - -fn tokenize_command(command: &str) -> Vec { - shlex::split(command).unwrap_or_else(|| { - command - .split_whitespace() - .map(std::string::ToString::to_string) - .collect() - }) -} - -pub(crate) async fn ensure_skill_approval_for_command( - sess: &Session, - turn_context: &TurnContext, - item_id: &str, - command: &str, - workdir: &Path, -) -> bool { - if !turn_context.features.enabled(Feature::SkillApproval) { - return true; - } - - let workdir = normalize_path(workdir); - let Some(skill) = detect_implicit_skill_script_invocation_for_command( - turn_context.turn_skills.outcome.as_ref(), - command, - workdir.as_path(), - ) else { - return true; - }; - - let cache_key = SkillApprovalCacheKey { - skill_name: skill.name.clone(), - skill_path: skill.path_to_skills_md.clone(), - skill_scope: skill.scope, - }; - let already_approved = { - let store = sess.services.tool_approvals.lock().await; - matches!( - store.get(&cache_key), - Some(ReviewDecision::ApprovedForSession) - ) - }; - if already_approved { - return true; - } - - let approved = sess - .request_skill_approval(turn_context, item_id.to_string(), skill.name) - .await - .is_some_and(|response| response.approved); - if !approved { - return false; - } - - let mut store = sess.services.tool_approvals.lock().await; - store.put(cache_key, ReviewDecision::ApprovedForSession); - true -} - pub(crate) async fn maybe_emit_implicit_skill_invocation( sess: &Session, turn_context: &TurnContext, @@ -207,6 +115,15 @@ pub(crate) async fn maybe_emit_implicit_skill_invocation( ); } +fn tokenize_command(command: &str) -> Vec { + shlex::split(command).unwrap_or_else(|| { + command + .split_whitespace() + .map(std::string::ToString::to_string) + .collect() + }) +} + fn script_run_token(tokens: &[String]) -> Option<&str> { const RUNNERS: [&str; 10] = [ "python", "python3", "bash", "zsh", "sh", "node", "deno", "ruby", "perl", "pwsh", @@ -317,7 +234,6 @@ fn normalize_path(path: &Path) -> PathBuf { mod tests { use super::SkillLoadOutcome; use super::SkillMetadata; - use super::detect_implicit_skill_script_invocation_for_command; use super::detect_skill_doc_read; use super::detect_skill_script_run; use super::normalize_path; @@ -336,6 +252,7 @@ mod tests { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: skill_doc_path, scope: codex_protocol::protocol::SkillScope::User, @@ -437,50 +354,4 @@ mod tests { Some("test-skill".to_string()) ); } - - #[test] - fn implicit_skill_script_invocation_matches_command() { - let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md"); - let scripts_dir = normalize_path(Path::new("/tmp/skill-test/scripts")); - let skill = test_skill_metadata(skill_doc_path); - let outcome = SkillLoadOutcome { - implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(scripts_dir, skill)])), - implicit_skills_by_doc_path: Arc::new(HashMap::new()), - ..Default::default() - }; - - let found = detect_implicit_skill_script_invocation_for_command( - &outcome, - "python scripts/fetch_comments.py", - Path::new("/tmp/skill-test"), - ); - - assert_eq!( - found.map(|value| value.name), - Some("test-skill".to_string()) - ); - } - - #[test] - fn implicit_skill_script_invocation_ignores_doc_reads() { - let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md"); - let normalized_skill_doc_path = normalize_path(skill_doc_path.as_path()); - let skill = test_skill_metadata(skill_doc_path); - let outcome = SkillLoadOutcome { - implicit_skills_by_scripts_dir: Arc::new(HashMap::new()), - implicit_skills_by_doc_path: Arc::new(HashMap::from([( - normalized_skill_doc_path, - skill, - )])), - ..Default::default() - }; - - let found = detect_implicit_skill_script_invocation_for_command( - &outcome, - "cat SKILL.md", - Path::new("/tmp/skill-test"), - ); - - assert_eq!(found, None); - } } diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index 960830649b..83db7a9078 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -12,11 +12,12 @@ use crate::skills::model::SkillLoadOutcome; use crate::skills::model::SkillMetadata; use crate::skills::model::SkillPolicy; use crate::skills::model::SkillToolDependency; -use crate::skills::permissions::SkillManifestPermissions; use crate::skills::permissions::compile_permission_profile; use crate::skills::system::system_cache_root_dir; use codex_app_server_protocol::ConfigLayerSource; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SkillScope; +use codex_utils_absolute_path::AbsolutePathBufGuard; use dirs::home_dir; use dunce::canonicalize as canonicalize_path; use serde::Deserialize; @@ -54,7 +55,16 @@ struct SkillMetadataFile { #[serde(default)] policy: Option, #[serde(default)] - permissions: Option, + permissions: Option, +} + +#[derive(Default)] +struct LoadedSkillMetadata { + interface: Option, + dependencies: Option, + policy: Option, + permission_profile: Option, + permissions: Option, } #[derive(Debug, Default, Deserialize)] @@ -506,7 +516,13 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result Result ( - Option, - Option, - Option, - Option, -) { +fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata { // Fail open: optional metadata should not block loading SKILL.md. let Some(skill_dir) = skill_path.parent() else { - return (None, None, None, None); + return LoadedSkillMetadata::default(); }; let metadata_path = skill_dir .join(SKILLS_METADATA_DIR) .join(SKILLS_METADATA_FILENAME); if !metadata_path.exists() { - return (None, None, None, None); + return LoadedSkillMetadata::default(); } let contents = match fs::read_to_string(&metadata_path) { @@ -560,19 +570,22 @@ fn load_skill_metadata( path = metadata_path.display(), label = SKILLS_METADATA_FILENAME ); - return (None, None, None, None); + return LoadedSkillMetadata::default(); } }; - let parsed: SkillMetadataFile = match serde_yaml::from_str(&contents) { - Ok(parsed) => parsed, - Err(error) => { - tracing::warn!( - "ignoring {path}: invalid {label}: {error}", - path = metadata_path.display(), - label = SKILLS_METADATA_FILENAME - ); - return (None, None, None, None); + let parsed: SkillMetadataFile = { + let _guard = AbsolutePathBufGuard::new(skill_dir); + match serde_yaml::from_str(&contents) { + Ok(parsed) => parsed, + Err(error) => { + tracing::warn!( + "ignoring {path}: invalid {label}: {error}", + path = metadata_path.display(), + label = SKILLS_METADATA_FILENAME + ); + return LoadedSkillMetadata::default(); + } } }; @@ -582,13 +595,15 @@ fn load_skill_metadata( policy, permissions, } = parsed; + let permission_profile = permissions.clone().filter(|profile| !profile.is_empty()); - ( - resolve_interface(interface, skill_dir), - resolve_dependencies(dependencies), - resolve_policy(policy), - compile_permission_profile(skill_dir, permissions), - ) + LoadedSkillMetadata { + interface: resolve_interface(interface, skill_dir), + dependencies: resolve_dependencies(dependencies), + policy: resolve_policy(policy), + permission_profile, + permissions: compile_permission_profile(skill_dir, permissions), + } } fn resolve_interface(interface: Option, skill_dir: &Path) -> Option { @@ -829,6 +844,8 @@ mod tests { use crate::config_loader::ConfigRequirementsToml; use codex_config::CONFIG_TOML_FILE; use codex_protocol::config_types::TrustLevel; + use codex_protocol::models::FileSystemPermissions; + use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -1048,6 +1065,7 @@ mod tests { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1196,6 +1214,7 @@ mod tests { ], }), policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1252,6 +1271,7 @@ interface: }), dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(skill_path.as_path()), scope: SkillScope::User, @@ -1341,7 +1361,6 @@ permissions: file_system: read: - "./data" - - "./data" write: - "./output" "#, @@ -1356,6 +1375,23 @@ permissions: outcome.errors ); assert_eq!(outcome.skills.len(), 1); + assert_eq!( + outcome.skills[0].permission_profile, + Some(PermissionProfile { + network: Some(true), + file_system: Some(FileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(normalized(skill_dir.join("data").as_path())) + .expect("absolute data path"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(normalized(skill_dir.join("output").as_path())) + .expect("absolute output path"), + ]), + }), + macos: None, + }) + ); #[cfg(target_os = "macos")] let macos_seatbelt_profile_extensions = Some(crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions::default()); @@ -1444,6 +1480,7 @@ permissions: {} windows_sandbox_mode: None, macos_seatbelt_profile_extensions: None, }); + assert_eq!(outcome.skills[0].permission_profile, None); assert_eq!(outcome.skills[0].permissions, expected); } @@ -1586,6 +1623,7 @@ permissions: }), dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1627,6 +1665,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1681,6 +1720,7 @@ permissions: }), dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1723,6 +1763,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1768,6 +1809,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&shared_skill_path), scope: SkillScope::User, @@ -1829,6 +1871,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -1866,6 +1909,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&shared_skill_path), scope: SkillScope::Admin, @@ -1907,6 +1951,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&linked_skill_path), scope: SkillScope::Repo, @@ -1975,6 +2020,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&within_depth_path), scope: SkillScope::User, @@ -2003,6 +2049,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -2035,6 +2082,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::User, @@ -2148,6 +2196,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::Repo, @@ -2184,6 +2233,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::Repo, @@ -2238,6 +2288,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&nested_skill_path), scope: SkillScope::Repo, @@ -2249,6 +2300,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&root_skill_path), scope: SkillScope::Repo, @@ -2289,6 +2341,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::Repo, @@ -2327,6 +2380,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::Repo, @@ -2369,6 +2423,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&repo_skill_path), scope: SkillScope::Repo, @@ -2380,6 +2435,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&user_skill_path), scope: SkillScope::User, @@ -2445,6 +2501,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: first_path, scope: SkillScope::Repo, @@ -2456,6 +2513,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: second_path, scope: SkillScope::Repo, @@ -2528,6 +2586,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::Repo, @@ -2587,6 +2646,7 @@ permissions: interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: normalized(&skill_path), scope: SkillScope::System, diff --git a/codex-rs/core/src/skills/manager.rs b/codex-rs/core/src/skills/manager.rs index fa00fac41f..7d2c6b89fd 100644 --- a/codex-rs/core/src/skills/manager.rs +++ b/codex-rs/core/src/skills/manager.rs @@ -130,6 +130,12 @@ impl SkillsManager { }), ); let mut outcome = load_skills_from_roots(roots); + if !extra_user_roots.is_empty() { + // When extra user roots are provided, skip system skills before caching the result. + outcome + .skills + .retain(|skill| skill.scope != SkillScope::System); + } outcome.disabled_paths = disabled_paths_from_stack(&config_layer_stack); let (by_scripts_dir, by_doc_path) = build_implicit_skill_path_indexes(outcome.allowed_skills_for_implicit_invocation()); diff --git a/codex-rs/core/src/skills/mod.rs b/codex-rs/core/src/skills/mod.rs index 6fccd71ccf..868a1d7def 100644 --- a/codex-rs/core/src/skills/mod.rs +++ b/codex-rs/core/src/skills/mod.rs @@ -14,9 +14,7 @@ pub(crate) use env_var_dependencies::resolve_skill_dependencies_for_turn; pub(crate) use injection::SkillInjections; pub(crate) use injection::build_skill_injections; pub(crate) use injection::collect_explicit_skill_mentions; -pub(crate) use invocation_utils::SKILL_APPROVAL_DECLINED_MESSAGE; pub(crate) use invocation_utils::build_implicit_skill_path_indexes; -pub(crate) use invocation_utils::ensure_skill_approval_for_command; pub(crate) use invocation_utils::maybe_emit_implicit_skill_invocation; pub use loader::load_skills; pub use manager::SkillsManager; diff --git a/codex-rs/core/src/skills/model.rs b/codex-rs/core/src/skills/model.rs index 536d2f4931..ddb1dd2638 100644 --- a/codex-rs/core/src/skills/model.rs +++ b/codex-rs/core/src/skills/model.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::sync::Arc; use crate::config::Permissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SkillScope; #[derive(Debug, Clone, PartialEq)] @@ -14,6 +15,7 @@ pub struct SkillMetadata { pub interface: Option, pub dependencies: Option, pub policy: Option, + pub permission_profile: Option, // This is an experimental field. pub permissions: Option, /// Path to the SKILLS.md file that declares this skill. diff --git a/codex-rs/core/src/skills/permissions.rs b/codex-rs/core/src/skills/permissions.rs index 24f3c9444c..38d02ee0b6 100644 --- a/codex-rs/core/src/skills/permissions.rs +++ b/codex-rs/core/src/skills/permissions.rs @@ -1,12 +1,15 @@ use std::collections::HashSet; -use std::path::Component; use std::path::Path; -use std::path::PathBuf; +#[cfg(target_os = "macos")] +use codex_protocol::models::MacOsAutomationValue; +use codex_protocol::models::MacOsPermissions; +#[cfg(target_os = "macos")] +use codex_protocol::models::MacOsPreferencesValue; +use codex_protocol::models::MacOsSeatbeltProfileExtensions; +use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; -use dirs::home_dir; use dunce::canonicalize as canonicalize_path; -use serde::Deserialize; use tracing::warn; use crate::config::Constrained; @@ -15,68 +18,23 @@ use crate::config::types::ShellEnvironmentPolicy; use crate::protocol::AskForApproval; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; -#[cfg(target_os = "macos")] -use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions; -#[cfg(not(target_os = "macos"))] -type MacOsSeatbeltProfileExtensions = (); - -#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] -pub(crate) struct SkillManifestPermissions { - #[serde(default)] - pub(crate) network: bool, - #[serde(default)] - pub(crate) file_system: SkillManifestFileSystemPermissions, - #[serde(default)] - pub(crate) macos: SkillManifestMacOsPermissions, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] -pub(crate) struct SkillManifestFileSystemPermissions { - #[serde(default)] - pub(crate) read: Vec, - #[serde(default)] - pub(crate) write: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)] -pub(crate) struct SkillManifestMacOsPermissions { - #[serde(default)] - pub(crate) preferences: Option, - #[serde(default)] - pub(crate) automations: Option, - #[serde(default)] - pub(crate) accessibility: bool, - #[serde(default)] - pub(crate) calendar: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -pub(crate) enum MacOsPreferencesValue { - Bool(bool), - Mode(String), -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -pub(crate) enum MacOsAutomationValue { - Bool(bool), - BundleIds(Vec), -} pub(crate) fn compile_permission_profile( - skill_dir: &Path, - permissions: Option, + _skill_dir: &Path, + permissions: Option, ) -> Option { - let permissions = permissions?; + let PermissionProfile { + network, + file_system, + macos, + } = permissions?; + let file_system = file_system.unwrap_or_default(); let fs_read = normalize_permission_paths( - skill_dir, - &permissions.file_system.read, + file_system.read.as_deref().unwrap_or_default(), "permissions.file_system.read", ); let fs_write = normalize_permission_paths( - skill_dir, - &permissions.file_system.write, + file_system.write.as_deref().unwrap_or_default(), "permissions.file_system.write", ); let sandbox_policy = if !fs_write.is_empty() { @@ -90,7 +48,7 @@ pub(crate) fn compile_permission_profile( readable_roots: fs_read, } }, - network_access: permissions.network, + network_access: network.unwrap_or_default(), exclude_tmpdir_env_var: false, exclude_slash_tmp: false, } @@ -105,8 +63,9 @@ pub(crate) fn compile_permission_profile( // Default sandbox policy SandboxPolicy::new_read_only_policy() }; + let macos_permissions = macos.unwrap_or_default(); let macos_seatbelt_profile_extensions = - build_macos_seatbelt_profile_extensions(&permissions.macos); + build_macos_seatbelt_profile_extensions(&macos_permissions); Some(Permissions { approval_policy: Constrained::allow_any(AskForApproval::Never), @@ -119,16 +78,12 @@ pub(crate) fn compile_permission_profile( }) } -fn normalize_permission_paths( - skill_dir: &Path, - values: &[String], - field: &str, -) -> Vec { +fn normalize_permission_paths(values: &[AbsolutePathBuf], field: &str) -> Vec { let mut paths = Vec::new(); let mut seen = HashSet::new(); for value in values { - let Some(path) = normalize_permission_path(skill_dir, value, field) else { + let Some(path) = normalize_permission_path(value, field) else { continue; }; if seen.insert(path.clone()) { @@ -139,26 +94,8 @@ fn normalize_permission_paths( paths } -fn normalize_permission_path( - skill_dir: &Path, - value: &str, - field: &str, -) -> Option { - let trimmed = value.trim(); - if trimmed.is_empty() { - warn!("ignoring {field}: value is empty"); - return None; - } - - let expanded = expand_home(trimmed); - let path = PathBuf::from(expanded); - let absolute = if path.is_absolute() { - path - } else { - skill_dir.join(path) - }; - let normalized = normalize_lexically(&absolute); - let canonicalized = canonicalize_path(&normalized).unwrap_or(normalized); +fn normalize_permission_path(value: &AbsolutePathBuf, field: &str) -> Option { + let canonicalized = canonicalize_path(value.as_path()).unwrap_or_else(|_| value.to_path_buf()); match AbsolutePathBuf::from_absolute_path(&canonicalized) { Ok(path) => Some(path), Err(error) => { @@ -168,24 +105,9 @@ fn normalize_permission_path( } } -fn expand_home(path: &str) -> String { - if path == "~" { - if let Some(home) = home_dir() { - return home.to_string_lossy().to_string(); - } - return path.to_string(); - } - if let Some(rest) = path.strip_prefix("~/") - && let Some(home) = home_dir() - { - return home.join(rest).to_string_lossy().to_string(); - } - path.to_string() -} - #[cfg(target_os = "macos")] fn build_macos_seatbelt_profile_extensions( - permissions: &SkillManifestMacOsPermissions, + permissions: &MacOsPermissions, ) -> Option { let defaults = MacOsSeatbeltProfileExtensions::default(); @@ -198,8 +120,10 @@ fn build_macos_seatbelt_profile_extensions( permissions.automations.as_ref(), defaults.macos_automation, ), - macos_accessibility: permissions.accessibility, - macos_calendar: permissions.calendar, + macos_accessibility: permissions + .accessibility + .unwrap_or(defaults.macos_accessibility), + macos_calendar: permissions.calendar.unwrap_or(defaults.macos_calendar), }; Some(extensions) } @@ -262,33 +186,13 @@ fn resolve_macos_automation_permission( #[cfg(not(target_os = "macos"))] fn build_macos_seatbelt_profile_extensions( - _: &SkillManifestMacOsPermissions, + _: &MacOsPermissions, ) -> Option { None } -fn normalize_lexically(path: &Path) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::ParentDir => { - normalized.pop(); - } - Component::RootDir | Component::Prefix(_) | Component::Normal(_) => { - normalized.push(component.as_os_str()); - } - } - } - normalized -} - #[cfg(test)] mod tests { - use super::SkillManifestFileSystemPermissions; - #[cfg(target_os = "macos")] - use super::SkillManifestMacOsPermissions; - use super::SkillManifestPermissions; use super::compile_permission_profile; use crate::config::Constrained; use crate::config::Permissions; @@ -296,9 +200,22 @@ mod tests { use crate::protocol::AskForApproval; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; + use codex_protocol::models::FileSystemPermissions; + #[cfg(target_os = "macos")] + use codex_protocol::models::MacOsAutomationValue; + #[cfg(target_os = "macos")] + use codex_protocol::models::MacOsPermissions; + #[cfg(target_os = "macos")] + use codex_protocol::models::MacOsPreferencesValue; + use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::fs; + use std::path::Path; + + fn absolute_path(path: &Path) -> AbsolutePathBuf { + AbsolutePathBuf::try_from(path).expect("absolute path") + } #[test] fn compile_permission_profile_normalizes_paths() { @@ -310,16 +227,16 @@ mod tests { let profile = compile_permission_profile( &skill_dir, - Some(SkillManifestPermissions { - network: true, - file_system: SkillManifestFileSystemPermissions { - read: vec![ - "./data".to_string(), - "./data".to_string(), - "scripts/../data".to_string(), - ], - write: vec!["./output".to_string()], - }, + Some(PermissionProfile { + network: Some(true), + file_system: Some(FileSystemPermissions { + read: Some(vec![ + absolute_path(&skill_dir.join("data")), + absolute_path(&skill_dir.join("data")), + absolute_path(&skill_dir.join("scripts/../data")), + ]), + write: Some(vec![absolute_path(&skill_dir.join("output"))]), + }), ..Default::default() }), ) @@ -380,8 +297,8 @@ mod tests { let profile = compile_permission_profile( &skill_dir, - Some(SkillManifestPermissions { - network: true, + Some(PermissionProfile { + network: Some(true), ..Default::default() }), ) @@ -415,12 +332,12 @@ mod tests { let profile = compile_permission_profile( &skill_dir, - Some(SkillManifestPermissions { - network: true, - file_system: SkillManifestFileSystemPermissions { - read: vec!["./data".to_string()], - write: Vec::new(), - }, + Some(PermissionProfile { + network: Some(true), + file_system: Some(FileSystemPermissions { + read: Some(vec![absolute_path(&skill_dir.join("data"))]), + write: Some(Vec::new()), + }), ..Default::default() }), ) @@ -464,15 +381,15 @@ mod tests { let profile = compile_permission_profile( &skill_dir, - Some(SkillManifestPermissions { - macos: SkillManifestMacOsPermissions { - preferences: Some(super::MacOsPreferencesValue::Mode("readwrite".to_string())), - automations: Some(super::MacOsAutomationValue::BundleIds(vec![ + Some(PermissionProfile { + macos: Some(MacOsPermissions { + preferences: Some(MacOsPreferencesValue::Mode("readwrite".to_string())), + automations: Some(MacOsAutomationValue::BundleIds(vec![ "com.apple.Notes".to_string(), ])), - accessibility: true, - calendar: true, - }, + accessibility: Some(true), + calendar: Some(true), + }), ..Default::default() }), ) @@ -502,9 +419,8 @@ mod tests { let skill_dir = tempdir.path().join("skill"); fs::create_dir_all(&skill_dir).expect("skill dir"); - let profile = - compile_permission_profile(&skill_dir, Some(SkillManifestPermissions::default())) - .expect("profile"); + let profile = compile_permission_profile(&skill_dir, Some(PermissionProfile::default())) + .expect("profile"); assert_eq!( profile.macos_seatbelt_profile_extensions, diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index 14ac226c58..f501551f95 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use crate::AuthManager; @@ -13,10 +14,12 @@ use crate::models_manager::manager::ModelsManager; use crate::skills::SkillsManager; use crate::state_db::StateDbHandle; use crate::tools::network_approval::NetworkApprovalService; +use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use codex_hooks::Hooks; use codex_otel::OtelManager; +use codex_utils_absolute_path::AbsolutePathBuf; use std::path::PathBuf; use tokio::sync::Mutex; use tokio::sync::RwLock; @@ -29,6 +32,8 @@ pub(crate) struct SessionServices { pub(crate) unified_exec_manager: UnifiedExecProcessManager, #[cfg_attr(not(unix), allow(dead_code))] pub(crate) shell_zsh_path: Option, + #[cfg_attr(not(unix), allow(dead_code))] + pub(crate) main_execve_wrapper_exe: Option, pub(crate) analytics_events_client: AnalyticsEventsClient, pub(crate) hooks: Hooks, pub(crate) rollout: Mutex>, @@ -40,6 +45,8 @@ pub(crate) struct SessionServices { pub(crate) models_manager: Arc, pub(crate) otel_manager: OtelManager, pub(crate) tool_approvals: Mutex, + #[cfg_attr(not(unix), allow(dead_code))] + pub(crate) execve_session_approvals: RwLock>, pub(crate) skills_manager: Arc, pub(crate) file_watcher: Arc, pub(crate) agent_control: AgentControl, diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index 3301740866..ccc50d066b 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -11,7 +11,6 @@ use tokio_util::task::AbortOnDropHandle; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::ResponseInputItem; use codex_protocol::request_user_input::RequestUserInputResponse; -use codex_protocol::skill_approval::SkillApprovalResponse; use tokio::sync::oneshot; use crate::codex::TurnContext; @@ -72,7 +71,6 @@ impl ActiveTurn { pub(crate) struct TurnState { pending_approvals: HashMap>, pending_user_input: HashMap>, - pending_skill_approvals: HashMap>, pending_dynamic_tools: HashMap>, pending_input: Vec, } @@ -96,7 +94,6 @@ impl TurnState { pub(crate) fn clear_pending(&mut self) { self.pending_approvals.clear(); self.pending_user_input.clear(); - self.pending_skill_approvals.clear(); self.pending_dynamic_tools.clear(); self.pending_input.clear(); } @@ -124,21 +121,6 @@ impl TurnState { self.pending_dynamic_tools.insert(key, tx) } - pub(crate) fn insert_pending_skill_approval( - &mut self, - key: String, - tx: oneshot::Sender, - ) -> Option> { - self.pending_skill_approvals.insert(key, tx) - } - - pub(crate) fn remove_pending_skill_approval( - &mut self, - key: &str, - ) -> Option> { - self.pending_skill_approvals.remove(key) - } - pub(crate) fn remove_pending_dynamic_tool( &mut self, key: &str, diff --git a/codex-rs/core/src/state_db.rs b/codex-rs/core/src/state_db.rs index 7f5b62a127..93c7e75f97 100644 --- a/codex-rs/core/src/state_db.rs +++ b/codex-rs/core/src/state_db.rs @@ -226,6 +226,7 @@ pub async fn list_threads_db( allowed_sources: &[SessionSource], model_providers: Option<&[String]>, archived: bool, + search_term: Option<&str>, ) -> Option { let ctx = context?; if ctx.codex_home() != codex_home { @@ -257,6 +258,7 @@ pub async fn list_threads_db( allowed_sources.as_slice(), model_providers.as_deref(), archived, + search_term, ) .await { @@ -343,6 +345,7 @@ pub async fn reconcile_rollout( builder: Option<&ThreadMetadataBuilder>, items: &[RolloutItem], archived_only: Option, + new_thread_memory_mode: Option<&str>, ) { let Some(ctx) = context else { return; @@ -355,6 +358,7 @@ pub async fn reconcile_rollout( builder, items, "reconcile_rollout", + new_thread_memory_mode, ) .await; return; @@ -465,6 +469,7 @@ pub async fn read_repair_rollout_path( None, &[], archived_only, + None, ) .await; } @@ -477,6 +482,7 @@ pub async fn apply_rollout_items( builder: Option<&ThreadMetadataBuilder>, items: &[RolloutItem], stage: &str, + new_thread_memory_mode: Option<&str>, ) { let Some(ctx) = context else { return; @@ -497,7 +503,10 @@ pub async fn apply_rollout_items( }; builder.rollout_path = rollout_path.to_path_buf(); builder.cwd = normalize_cwd_for_state_db(&builder.cwd); - if let Err(err) = ctx.apply_rollout_items(&builder, items, None).await { + if let Err(err) = ctx + .apply_rollout_items(&builder, items, None, new_thread_memory_mode) + .await + { warn!( "state db apply_rollout_items failed during {stage} for {}: {err}", rollout_path.display() diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 394f4b9329..fe72db09f1 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use codex_protocol::config_types::ModeKind; use codex_protocol::items::TurnItem; +use codex_utils_stream_parser::strip_citations; use tokio_util::sync::CancellationToken; use crate::codex::Session; @@ -10,18 +11,75 @@ use crate::codex::TurnContext; use crate::error::CodexErr; use crate::error::Result; use crate::function_tool::FunctionCallError; +use crate::memories::citations::get_thread_id_from_citations; use crate::parse_turn_item; -use crate::proposed_plan_parser::strip_proposed_plan_blocks; +use crate::state_db; use crate::tools::parallel::ToolCallRuntime; use crate::tools::router::ToolRouter; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_utils_stream_parser::strip_proposed_plan_blocks; use futures::Future; use tracing::debug; use tracing::instrument; +fn strip_hidden_assistant_markup(text: &str, plan_mode: bool) -> String { + let (without_citations, _) = strip_citations(text); + if plan_mode { + strip_proposed_plan_blocks(&without_citations) + } else { + without_citations + } +} + +pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option { + if let ResponseItem::Message { role, content, .. } = item + && role == "assistant" + { + let combined = content + .iter() + .filter_map(|ci| match ci { + codex_protocol::models::ContentItem::OutputText { text } => Some(text.as_str()), + _ => None, + }) + .collect::(); + return Some(combined); + } + None +} + +/// Persist a completed model response item and record any cited memory usage. +pub(crate) async fn record_completed_response_item( + sess: &Session, + turn_context: &TurnContext, + item: &ResponseItem, +) { + sess.record_conversation_items(turn_context, std::slice::from_ref(item)) + .await; + record_stage1_output_usage_for_completed_item(turn_context, item).await; +} + +async fn record_stage1_output_usage_for_completed_item( + turn_context: &TurnContext, + item: &ResponseItem, +) { + let Some(raw_text) = raw_assistant_output_text_from_item(item) else { + return; + }; + + let (_, citations) = strip_citations(&raw_text); + let thread_ids = get_thread_id_from_citations(citations); + if thread_ids.is_empty() { + return; + } + + if let Some(db) = state_db::get_state_db(turn_context.config.as_ref(), None).await { + let _ = db.record_stage1_output_usage(&thread_ids).await; + } +} + /// Handle a completed output item from the model stream, recording it and /// queuing any tool execution futures. This records items immediately so /// history and rollout stay in sync even if the turn is later cancelled. @@ -62,8 +120,7 @@ pub(crate) async fn handle_output_item_done( payload_preview ); - ctx.sess - .record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item)) + record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item) .await; let cancellation_token = ctx.cancellation_token.child_token(); @@ -78,7 +135,7 @@ pub(crate) async fn handle_output_item_done( } // No tool call: convert messages/reasoning into turn items and mark them as complete. Ok(None) => { - if let Some(turn_item) = handle_non_tool_response_item(&item, plan_mode).await { + if let Some(turn_item) = handle_non_tool_response_item(&item, plan_mode) { if previously_active_item.is_none() { ctx.sess .emit_turn_item_started(&ctx.turn_context, &turn_item) @@ -90,8 +147,7 @@ pub(crate) async fn handle_output_item_done( .await; } - ctx.sess - .record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item)) + record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item) .await; let last_agent_message = last_assistant_message_from_item(&item, plan_mode); @@ -112,8 +168,7 @@ pub(crate) async fn handle_output_item_done( ..Default::default() }, }; - ctx.sess - .record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item)) + record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item) .await; if let Some(response_item) = response_input_to_response_item(&response) { ctx.sess @@ -135,8 +190,7 @@ pub(crate) async fn handle_output_item_done( ..Default::default() }, }; - ctx.sess - .record_conversation_items(&ctx.turn_context, std::slice::from_ref(&item)) + record_completed_response_item(ctx.sess.as_ref(), ctx.turn_context.as_ref(), &item) .await; if let Some(response_item) = response_input_to_response_item(&response) { ctx.sess @@ -158,7 +212,7 @@ pub(crate) async fn handle_output_item_done( Ok(output) } -pub(crate) async fn handle_non_tool_response_item( +pub(crate) fn handle_non_tool_response_item( item: &ResponseItem, plan_mode: bool, ) -> Option { @@ -169,7 +223,7 @@ pub(crate) async fn handle_non_tool_response_item( | ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } => { let mut turn_item = parse_turn_item(item)?; - if plan_mode && let TurnItem::AgentMessage(agent_message) = &mut turn_item { + if let TurnItem::AgentMessage(agent_message) = &mut turn_item { let combined = agent_message .content .iter() @@ -177,7 +231,7 @@ pub(crate) async fn handle_non_tool_response_item( codex_protocol::items::AgentMessageContent::Text { text } => text.as_str(), }) .collect::(); - let stripped = strip_proposed_plan_blocks(&combined); + let stripped = strip_hidden_assistant_markup(&combined, plan_mode); agent_message.content = vec![codex_protocol::items::AgentMessageContent::Text { text: stripped }]; } @@ -195,25 +249,15 @@ pub(crate) fn last_assistant_message_from_item( item: &ResponseItem, plan_mode: bool, ) -> Option { - if let ResponseItem::Message { role, content, .. } = item - && role == "assistant" - { - let combined = content - .iter() - .filter_map(|ci| match ci { - codex_protocol::models::ContentItem::OutputText { text } => Some(text.as_str()), - _ => None, - }) - .collect::(); + if let Some(combined) = raw_assistant_output_text_from_item(item) { if combined.is_empty() { return None; } - return if plan_mode { - let stripped = strip_proposed_plan_blocks(&combined); - (!stripped.trim().is_empty()).then_some(stripped) - } else { - Some(combined) - }; + let stripped = strip_hidden_assistant_markup(&combined, plan_mode); + if stripped.trim().is_empty() { + return None; + } + return Some(stripped); } None } @@ -248,3 +292,71 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti _ => None, } } + +#[cfg(test)] +mod tests { + use super::handle_non_tool_response_item; + use super::last_assistant_message_from_item; + use codex_protocol::items::TurnItem; + use codex_protocol::models::ContentItem; + use codex_protocol::models::ResponseItem; + use pretty_assertions::assert_eq; + + fn assistant_output_text(text: &str) -> ResponseItem { + ResponseItem::Message { + id: Some("msg-1".to_string()), + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: text.to_string(), + }], + end_turn: Some(true), + phase: None, + } + } + + #[test] + fn handle_non_tool_response_item_strips_citations_from_assistant_message() { + let item = assistant_output_text("hellodoc1 world"); + + let turn_item = + handle_non_tool_response_item(&item, false).expect("assistant message should parse"); + + let TurnItem::AgentMessage(agent_message) = turn_item else { + panic!("expected agent message"); + }; + let text = agent_message + .content + .iter() + .map(|entry| match entry { + codex_protocol::items::AgentMessageContent::Text { text } => text.as_str(), + }) + .collect::(); + assert_eq!(text, "hello world"); + } + + #[test] + fn last_assistant_message_from_item_strips_citations_and_plan_blocks() { + let item = assistant_output_text( + "beforedoc1\n\n- x\n\nafter", + ); + + let message = last_assistant_message_from_item(&item, true) + .expect("assistant text should remain after stripping"); + + assert_eq!(message, "before\nafter"); + } + + #[test] + fn last_assistant_message_from_item_returns_none_for_citation_only_message() { + let item = assistant_output_text("doc1"); + + assert_eq!(last_assistant_message_from_item(&item, false), None); + } + + #[test] + fn last_assistant_message_from_item_returns_none_for_plan_only_hidden_message() { + let item = assistant_output_text("\n- x\n"); + + assert_eq!(last_assistant_message_from_item(&item, true), None); + } +} diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 861b18a52c..97719f1049 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -21,12 +21,12 @@ use tracing::warn; use crate::AuthManager; use crate::codex::Session; use crate::codex::TurnContext; +use crate::contextual_user_message::TURN_ABORTED_OPEN_TAG; use crate::models_manager::manager::ModelsManager; use crate::protocol::EventMsg; use crate::protocol::TurnAbortReason; use crate::protocol::TurnAbortedEvent; use crate::protocol::TurnCompleteEvent; -use crate::session_prefix::TURN_ABORTED_OPEN_TAG; use crate::state::ActiveTurn; use crate::state::RunningTask; use crate::state::TaskKind; diff --git a/codex-rs/core/src/terminal.rs b/codex-rs/core/src/terminal.rs index 8437962f6c..b91aef106b 100644 --- a/codex-rs/core/src/terminal.rs +++ b/codex-rs/core/src/terminal.rs @@ -249,6 +249,7 @@ pub fn terminal_info() -> TerminalInfo { /// type is split on whitespace to extract a program name plus optional version (for example, /// `ghostty 1.2.3`), while the client term name becomes the `TERM` capability string. /// - Otherwise, `TERM_PROGRAM` (plus `TERM_PROGRAM_VERSION`) drives the detected terminal name. +/// This means `TERM_PROGRAM` can mask later probes (for example `WT_SESSION`). /// - Next, terminal-specific variables (WEZTERM, iTerm2, Apple Terminal, kitty, etc.) are checked. /// - Finally, `TERM` is used as the capability fallback with `TerminalName::Unknown`. /// diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 5e6d611adc..12ba7cda82 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -85,5 +85,7 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets() + collaboration_mode_presets::builtin_collaboration_mode_presets( + collaboration_mode_presets::CollaborationModesConfig::default(), + ) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index cb7c464362..0a56cab1d6 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -11,6 +11,7 @@ use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::file_watcher::FileWatcher; use crate::file_watcher::FileWatcherEvent; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::models_manager::manager::ModelsManager; use crate::protocol::Event; use crate::protocol::EventMsg; @@ -143,6 +144,7 @@ impl ThreadManager { auth_manager: Arc, session_source: SessionSource, model_catalog: Option, + collaboration_modes_config: CollaborationModesConfig, ) -> Self { let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let skills_manager = Arc::new(SkillsManager::new(codex_home.clone())); @@ -155,6 +157,7 @@ impl ThreadManager { codex_home, auth_manager.clone(), model_catalog, + collaboration_modes_config, )), skills_manager, file_watcher, @@ -249,7 +252,7 @@ impl ThreadManager { } pub async fn list_thread_ids(&self) -> Vec { - self.state.threads.read().await.keys().copied().collect() + self.state.list_thread_ids().await } pub async fn refresh_mcp_servers(&self, refresh_config: McpServerRefreshConfig) { @@ -291,6 +294,22 @@ impl ThreadManager { config: Config, dynamic_tools: Vec, persist_extended_history: bool, + ) -> CodexResult { + self.start_thread_with_tools_and_service_name( + config, + dynamic_tools, + persist_extended_history, + None, + ) + .await + } + + pub async fn start_thread_with_tools_and_service_name( + &self, + config: Config, + dynamic_tools: Vec, + persist_extended_history: bool, + metrics_service_name: Option, ) -> CodexResult { self.state .spawn_thread( @@ -300,6 +319,7 @@ impl ThreadManager { self.agent_control(), dynamic_tools, persist_extended_history, + metrics_service_name, ) .await } @@ -330,6 +350,7 @@ impl ThreadManager { self.agent_control(), Vec::new(), persist_extended_history, + None, ) .await } @@ -371,6 +392,7 @@ impl ThreadManager { self.agent_control(), Vec::new(), persist_extended_history, + None, ) .await } @@ -390,6 +412,10 @@ impl ThreadManager { } impl ThreadManagerState { + pub(crate) async fn list_thread_ids(&self) -> Vec { + self.threads.read().await.keys().copied().collect() + } + /// Fetch a thread by ID or return ThreadNotFound. pub(crate) async fn get_thread(&self, thread_id: ThreadId) -> CodexResult> { let threads = self.threads.read().await; @@ -421,8 +447,14 @@ impl ThreadManagerState { config: Config, agent_control: AgentControl, ) -> CodexResult { - self.spawn_new_thread_with_source(config, agent_control, self.session_source.clone(), false) - .await + self.spawn_new_thread_with_source( + config, + agent_control, + self.session_source.clone(), + false, + None, + ) + .await } pub(crate) async fn spawn_new_thread_with_source( @@ -431,6 +463,7 @@ impl ThreadManagerState { agent_control: AgentControl, session_source: SessionSource, persist_extended_history: bool, + metrics_service_name: Option, ) -> CodexResult { self.spawn_thread_with_source( config, @@ -440,6 +473,7 @@ impl ThreadManagerState { session_source, Vec::new(), persist_extended_history, + metrics_service_name, ) .await } @@ -460,11 +494,34 @@ impl ThreadManagerState { session_source, Vec::new(), false, + None, + ) + .await + } + + pub(crate) async fn fork_thread_with_source( + &self, + config: Config, + initial_history: InitialHistory, + agent_control: AgentControl, + session_source: SessionSource, + persist_extended_history: bool, + ) -> CodexResult { + self.spawn_thread_with_source( + config, + initial_history, + Arc::clone(&self.auth_manager), + agent_control, + session_source, + Vec::new(), + persist_extended_history, + None, ) .await } /// Spawn a new thread with optional history and register it with the manager. + #[allow(clippy::too_many_arguments)] pub(crate) async fn spawn_thread( &self, config: Config, @@ -473,6 +530,7 @@ impl ThreadManagerState { agent_control: AgentControl, dynamic_tools: Vec, persist_extended_history: bool, + metrics_service_name: Option, ) -> CodexResult { self.spawn_thread_with_source( config, @@ -482,6 +540,7 @@ impl ThreadManagerState { self.session_source.clone(), dynamic_tools, persist_extended_history, + metrics_service_name, ) .await } @@ -496,6 +555,7 @@ impl ThreadManagerState { session_source: SessionSource, dynamic_tools: Vec, persist_extended_history: bool, + metrics_service_name: Option, ) -> CodexResult { let watch_registration = self.file_watcher.register_config(&config); let CodexSpawnOk { @@ -511,6 +571,7 @@ impl ThreadManagerState { agent_control, dynamic_tools, persist_extended_history, + metrics_service_name, ) .await?; self.finalize_thread_spawn(codex, thread_id, watch_registration) @@ -654,7 +715,7 @@ mod tests { #[tokio::test] async fn ignores_session_prefix_messages_when_truncating() { let (session, turn_context) = make_session_and_context().await; - let mut items = session.build_initial_context(&turn_context).await; + let mut items = session.build_initial_context(&turn_context, None).await; items.push(user_msg("feature request")); items.push(assistant_msg("ack")); items.push(user_msg("second question")); diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index 081d066873..0700b4d013 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -30,7 +30,6 @@ pub struct ToolInvocation { pub call_id: String, pub tool_name: String, pub payload: ToolPayload, - pub source: ToolCallSource, } #[derive(Clone, Debug)] @@ -96,15 +95,12 @@ impl ToolOutput { match self { ToolOutput::Function { body, success } => { // `custom_tool_call` is the Responses API item type for freeform - // tools (`ToolSpec::Freeform`, e.g. freeform `apply_patch`). - // Those payloads must round-trip as `custom_tool_call_output` - // with plain string output. + // tools (`ToolSpec::Freeform`, e.g. freeform `apply_patch` or + // `js_repl`). if matches!(payload, ToolPayload::Custom { .. }) { - // Freeform/custom tools (`custom_tool_call`) use the custom - // output wire shape and remain string-only. return ResponseInputItem::CustomToolCallOutput { call_id: call_id.to_string(), - output: body.to_text().unwrap_or_default(), + output: FunctionCallOutputPayload { body, success }, }; } @@ -184,7 +180,9 @@ mod tests { match response { ResponseInputItem::CustomToolCallOutput { call_id, output } => { assert_eq!(call_id, "call-42"); - assert_eq!(output, "patched"); + assert_eq!(output.text_content(), Some("patched")); + assert!(output.content_items().is_none()); + assert_eq!(output.success, Some(true)); } other => panic!("expected CustomToolCallOutput, got {other:?}"), } @@ -235,8 +233,21 @@ mod tests { match response { ResponseInputItem::CustomToolCallOutput { call_id, output } => { + let expected = vec![ + FunctionCallOutputContentItem::InputText { + text: "line 1".to_string(), + }, + FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,AAA".to_string(), + }, + FunctionCallOutputContentItem::InputText { + text: "line 2".to_string(), + }, + ]; assert_eq!(call_id, "call-99"); - assert_eq!(output, "line 1\nline 2"); + assert_eq!(output.content_items(), Some(expected.as_slice())); + assert_eq!(output.body.to_text().as_deref(), Some("line 1\nline 2")); + assert_eq!(output.success, Some(true)); } other => panic!("expected CustomToolCallOutput, got {other:?}"), } diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 6b36be05dd..c4891a6b3a 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -1,5 +1,4 @@ use crate::agent::exceeds_thread_spawn_depth_limit; -use crate::agent::max_thread_spawn_depth; use crate::agent::next_thread_spawn_depth; use crate::agent::status::is_final; use crate::codex::Session; @@ -531,7 +530,7 @@ async fn build_runner_options( ) -> Result { let session_source = turn.session_source.clone(); let child_depth = next_thread_spawn_depth(&session_source); - let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth); + let max_depth = turn.config.agent_max_depth; if exceeds_thread_spawn_depth_limit(child_depth, max_depth) { return Err(FunctionCallError::RespondToModel( "agent depth limit reached; this session cannot spawn more subagents".to_string(), @@ -540,7 +539,7 @@ async fn build_runner_options( let max_concurrency = normalize_concurrency(requested_concurrency, turn.config.agent_max_threads); let base_instructions = session.get_base_instructions().await; - let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref(), child_depth)?; + let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?; Ok(JobRunnerOptions { max_concurrency, spawn_config, diff --git a/codex-rs/core/src/tools/handlers/dynamic.rs b/codex-rs/core/src/tools/handlers/dynamic.rs index 51dc8db56f..23c8474b9f 100644 --- a/codex-rs/core/src/tools/handlers/dynamic.rs +++ b/codex-rs/core/src/tools/handlers/dynamic.rs @@ -12,8 +12,10 @@ use codex_protocol::dynamic_tools::DynamicToolCallRequest; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::protocol::DynamicToolCallResponseEvent; use codex_protocol::protocol::EventMsg; use serde_json::Value; +use std::time::Instant; use tokio::sync::oneshot; use tracing::warn; @@ -81,7 +83,7 @@ async fn request_dynamic_tool( tool: String, arguments: Value, ) -> Option { - let _sub_id = turn_context.sub_id.clone(); + let turn_id = turn_context.sub_id.clone(); let (tx_response, rx_response) = oneshot::channel(); let event_id = call_id.clone(); let prev_entry = { @@ -98,12 +100,39 @@ async fn request_dynamic_tool( warn!("Overwriting existing pending dynamic tool call for call_id: {event_id}"); } + let started_at = Instant::now(); let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest { - call_id, - turn_id: turn_context.sub_id.clone(), - tool, - arguments, + call_id: call_id.clone(), + turn_id: turn_id.clone(), + tool: tool.clone(), + arguments: arguments.clone(), }); session.send_event(turn_context, event).await; - rx_response.await.ok() + let response = rx_response.await.ok(); + + let response_event = match &response { + Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id, + turn_id, + tool, + arguments, + content_items: response.content_items.clone(), + success: response.success, + error: None, + duration: started_at.elapsed(), + }), + None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent { + call_id, + turn_id, + tool, + arguments, + content_items: Vec::new(), + success: false, + error: Some("dynamic tool call was cancelled before receiving a response".to_string()), + duration: started_at.elapsed(), + }), + }; + session.send_event(turn_context, response_event).await; + + response } diff --git a/codex-rs/core/src/tools/handlers/js_repl.rs b/codex-rs/core/src/tools/handlers/js_repl.rs index 4488b4ea51..362d25b81e 100644 --- a/codex-rs/core/src/tools/handlers/js_repl.rs +++ b/codex-rs/core/src/tools/handlers/js_repl.rs @@ -155,9 +155,13 @@ impl ToolHandler for JsReplHandler { }; let content = result.output; - let items = vec![FunctionCallOutputContentItem::InputText { - text: content.clone(), - }]; + let mut items = Vec::with_capacity(result.content_items.len() + 1); + if !content.is_empty() { + items.push(FunctionCallOutputContentItem::InputText { + text: content.clone(), + }); + } + items.extend(result.content_items); emit_js_repl_exec_end( session.as_ref(), @@ -170,7 +174,11 @@ impl ToolHandler for JsReplHandler { .await; Ok(ToolOutput::Function { - body: FunctionCallOutputBody::ContentItems(items), + body: if items.is_empty() { + FunctionCallOutputBody::Text(content) + } else { + FunctionCallOutputBody::ContentItems(items) + }, success: Some(true), }) } diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index bdc47a088d..89e35fa227 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -16,15 +16,18 @@ mod test_sync; pub(crate) mod unified_exec; mod view_image; +use codex_utils_absolute_path::AbsolutePathBufGuard; pub use plan::PLAN_TOOL; use serde::Deserialize; +use serde_json::Value; use std::path::Path; +use std::path::PathBuf; use crate::function_tool::FunctionCallError; use crate::sandboxing::SandboxPermissions; use crate::sandboxing::normalize_additional_permissions; pub use apply_patch::ApplyPatchHandler; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; pub use dynamic::DynamicToolHandler; pub use grep_files::GrepFilesHandler; @@ -56,15 +59,42 @@ where }) } +fn parse_arguments_with_base_path( + arguments: &str, + base_path: &Path, +) -> Result +where + T: for<'de> Deserialize<'de>, +{ + let _guard = AbsolutePathBufGuard::new(base_path); + parse_arguments(arguments) +} + +fn resolve_workdir_base_path( + arguments: &str, + default_cwd: &Path, +) -> Result { + let arguments: Value = parse_arguments(arguments)?; + Ok(arguments + .get("workdir") + .and_then(Value::as_str) + .filter(|workdir| !workdir.is_empty()) + .map(PathBuf::from) + .map_or_else( + || default_cwd.to_path_buf(), + |workdir| crate::util::resolve_path(default_cwd, &workdir), + )) +} + /// Validates feature/policy constraints for `with_additional_permissions` and /// returns normalized absolute paths. Errors if paths are invalid. pub(super) fn normalize_and_validate_additional_permissions( request_permission_enabled: bool, approval_policy: AskForApproval, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, - cwd: &Path, -) -> Result, String> { + additional_permissions: Option, + _cwd: &Path, +) -> Result, String> { let uses_additional_permissions = matches!( sandbox_permissions, SandboxPermissions::WithAdditionalPermissions @@ -87,14 +117,14 @@ pub(super) fn normalize_and_validate_additional_permissions( } let Some(additional_permissions) = additional_permissions else { return Err( - "missing `additional_permissions`; provide `fs_read` and/or `fs_write` when using `with_additional_permissions`" + "missing `additional_permissions`; provide `file_system.read` and/or `file_system.write` when using `with_additional_permissions`" .to_string(), ); }; - let normalized = normalize_additional_permissions(additional_permissions, cwd)?; + let normalized = normalize_additional_permissions(additional_permissions)?; if normalized.is_empty() { return Err( - "`additional_permissions` must include at least one path in `fs_read` or `fs_write`" + "`additional_permissions` must include at least one path in `file_system.read` or `file_system.write`" .to_string(), ); } diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 8fbbd0d112..076a496a49 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -1,10 +1,8 @@ use crate::agent::AgentStatus; use crate::agent::exceeds_thread_spawn_depth_limit; -use crate::agent::max_thread_spawn_depth; use crate::codex::Session; use crate::codex::TurnContext; use crate::config::Config; -use crate::config::Constrained; use crate::error::CodexErr; use crate::features::Feature; use crate::function_tool::FunctionCallError; @@ -18,7 +16,6 @@ use async_trait::async_trait; use codex_protocol::ThreadId; use codex_protocol::models::BaseInstructions; use codex_protocol::models::FunctionCallOutputBody; -use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CollabAgentInteractionBeginEvent; use codex_protocol::protocol::CollabAgentInteractionEndEvent; use codex_protocol::protocol::CollabAgentRef; @@ -94,11 +91,11 @@ impl ToolHandler for MultiAgentHandler { mod spawn { use super::*; + use crate::agent::control::SpawnAgentOptions; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; use crate::agent::exceeds_thread_spawn_depth_limit; - use crate::agent::max_thread_spawn_depth; use crate::agent::next_thread_spawn_depth; use std::sync::Arc; @@ -107,6 +104,8 @@ mod spawn { message: Option, items: Option>, agent_type: Option, + #[serde(default)] + fork_context: bool, } #[derive(Debug, Serialize)] @@ -131,7 +130,7 @@ mod spawn { let prompt = input_preview(&input_items); let session_source = turn.session_source.clone(); let child_depth = next_thread_spawn_depth(&session_source); - let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth); + let max_depth = turn.config.agent_max_depth; if exceeds_thread_spawn_depth_limit(child_depth, max_depth) { return Err(FunctionCallError::RespondToModel( "Agent depth limit reached. Solve the task yourself.".to_string(), @@ -148,11 +147,8 @@ mod spawn { .into(), ) .await; - let mut config = build_agent_spawn_config( - &session.get_base_instructions().await, - turn.as_ref(), - child_depth, - )?; + let mut config = + build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?; apply_role_to_config(&mut config, role_name) .await .map_err(FunctionCallError::RespondToModel)?; @@ -162,7 +158,7 @@ mod spawn { let result = session .services .agent_control - .spawn_agent( + .spawn_agent_with_options( config, input_items, Some(thread_spawn_source( @@ -170,6 +166,9 @@ mod spawn { child_depth, role_name, )), + SpawnAgentOptions { + fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()), + }, ) .await .map_err(collab_spawn_error); @@ -348,7 +347,7 @@ mod resume_agent { .await .unwrap_or((None, None)); let child_depth = next_thread_spawn_depth(&turn.session_source); - let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth); + let max_depth = turn.config.agent_max_depth; if exceeds_thread_spawn_depth_limit(child_depth, max_depth) { return Err(FunctionCallError::RespondToModel( "Agent depth limit reached. Solve the task yourself.".to_string(), @@ -898,9 +897,8 @@ fn input_preview(items: &[UserInput]) -> String { pub(crate) fn build_agent_spawn_config( base_instructions: &BaseInstructions, turn: &TurnContext, - child_depth: i32, ) -> Result { - let mut config = build_agent_shared_config(turn, child_depth)?; + let mut config = build_agent_shared_config(turn)?; config.base_instructions = Some(base_instructions.text.clone()); Ok(config) } @@ -909,26 +907,23 @@ fn build_agent_resume_config( turn: &TurnContext, child_depth: i32, ) -> Result { - let mut config = build_agent_shared_config(turn, child_depth)?; + let mut config = build_agent_shared_config(turn)?; + apply_spawn_agent_overrides(&mut config, child_depth); // For resume, keep base instructions sourced from rollout/session metadata. config.base_instructions = None; Ok(config) } -fn build_agent_shared_config( - turn: &TurnContext, - child_depth: i32, -) -> Result { +fn build_agent_shared_config(turn: &TurnContext) -> Result { let base_config = turn.config.clone(); let mut config = (*base_config).clone(); config.model = Some(turn.model_info.slug.clone()); config.model_provider = turn.provider.clone(); config.model_reasoning_effort = turn.reasoning_effort; - config.model_reasoning_summary = turn.reasoning_summary; + config.model_reasoning_summary = Some(turn.reasoning_summary); config.developer_instructions = turn.developer_instructions.clone(); config.compact_prompt = turn.compact_prompt.clone(); apply_spawn_agent_runtime_overrides(&mut config, turn)?; - apply_spawn_agent_overrides(&mut config, child_depth); Ok(config) } @@ -937,6 +932,13 @@ fn apply_spawn_agent_runtime_overrides( config: &mut Config, turn: &TurnContext, ) -> Result<(), FunctionCallError> { + config + .permissions + .approval_policy + .set(turn.approval_policy.value()) + .map_err(|err| { + FunctionCallError::RespondToModel(format!("approval_policy is invalid: {err}")) + })?; config.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone(); config.cwd = turn.cwd.clone(); @@ -951,9 +953,7 @@ fn apply_spawn_agent_runtime_overrides( } fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) { - config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); - let max_depth = max_thread_spawn_depth(config.agent_max_spawn_depth); - if exceeds_thread_spawn_depth_limit(child_depth + 1, max_depth) { + if child_depth >= config.agent_max_depth { config.features.disable(Feature::Collab); } } @@ -964,7 +964,6 @@ mod tests { use crate::AuthManager; use crate::CodexAuth; use crate::ThreadManager; - use crate::agent::max_thread_spawn_depth; use crate::built_in_model_providers; use crate::codex::make_session_and_context; use crate::config::DEFAULT_AGENT_MAX_DEPTH; @@ -1004,7 +1003,6 @@ mod tests { call_id: "call-1".to_string(), tool_name: tool_name.to_string(), payload, - source: crate::tools::router::ToolCallSource::Direct, } } @@ -1105,8 +1103,7 @@ mod tests { } #[tokio::test] - #[ignore = "No role requiring it for now"] - async fn spawn_agent_uses_explorer_role_and_sets_never_approval_policy() { + async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() { #[derive(Debug, Deserialize)] struct SpawnAgentResult { agent_id: String, @@ -1122,6 +1119,9 @@ mod tests { .approval_policy .set(AskForApproval::OnRequest) .expect("approval policy should be set"); + turn.approval_policy + .set(AskForApproval::OnRequest) + .expect("approval policy should be set"); turn.config = Arc::new(config); let invocation = invocation( @@ -1159,8 +1159,7 @@ mod tests { .expect("spawned agent thread should exist") .config_snapshot() .await; - assert_eq!(snapshot.model, "gpt-5.1-codex-mini"); - assert_eq!(snapshot.approval_policy, AskForApproval::Never); + assert_eq!(snapshot.approval_policy, AskForApproval::OnRequest); } #[tokio::test] @@ -1211,6 +1210,9 @@ mod tests { &turn.config.permissions.sandbox_policy, turn.config.permissions.sandbox_policy.get().clone(), ); + turn.approval_policy + .set(AskForApproval::OnRequest) + .expect("approval policy should be set"); turn.sandbox_policy .set(expected_sandbox.clone()) .expect("sandbox policy should be set"); @@ -1226,7 +1228,7 @@ mod tests { "spawn_agent", function_payload(json!({ "message": "await this command", - "agent_type": "awaiter" + "agent_type": "explorer" })), ); let output = MultiAgentHandler @@ -1257,7 +1259,7 @@ mod tests { .config_snapshot() .await; assert_eq!(snapshot.sandbox_policy, expected_sandbox); - assert_eq!(snapshot.approval_policy, AskForApproval::Never); + assert_eq!(snapshot.approval_policy, AskForApproval::OnRequest); } #[tokio::test] @@ -1266,7 +1268,7 @@ mod tests { let manager = thread_manager(); session.services.agent_control = manager.agent_control(); - let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth); + let max_depth = turn.config.agent_max_depth; turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: session.conversation_id, depth: max_depth, @@ -1697,7 +1699,7 @@ mod tests { let manager = thread_manager(); session.services.agent_control = manager.agent_control(); - let max_depth = max_thread_spawn_depth(turn.config.agent_max_spawn_depth); + let max_depth = turn.config.agent_max_depth; turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: session.conversation_id, depth: max_depth, @@ -2040,14 +2042,17 @@ mod tests { turn.sandbox_policy .set(sandbox_policy) .expect("sandbox policy set"); + turn.approval_policy + .set(AskForApproval::OnRequest) + .expect("approval policy set"); - let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); let mut expected = (*turn.config).clone(); expected.base_instructions = Some(base_instructions.text); expected.model = Some(turn.model_info.slug.clone()); expected.model_provider = turn.provider.clone(); expected.model_reasoning_effort = turn.reasoning_effort; - expected.model_reasoning_summary = turn.reasoning_summary; + expected.model_reasoning_summary = Some(turn.reasoning_summary); expected.developer_instructions = turn.developer_instructions.clone(); expected.compact_prompt = turn.compact_prompt.clone(); expected.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); @@ -2056,7 +2061,7 @@ mod tests { expected .permissions .approval_policy - .set(AskForApproval::Never) + .set(AskForApproval::OnRequest) .expect("approval policy set"); expected .permissions @@ -2077,7 +2082,7 @@ mod tests { text: "base".to_string(), }; - let config = build_agent_spawn_config(&base_instructions, &turn, 0).expect("spawn config"); + let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config"); assert_eq!(config.user_instructions, base_config.user_instructions); } @@ -2088,6 +2093,9 @@ mod tests { let mut base_config = (*turn.config).clone(); base_config.base_instructions = Some("caller-base".to_string()); turn.config = Arc::new(base_config); + turn.approval_policy + .set(AskForApproval::OnRequest) + .expect("approval policy set"); let config = build_agent_resume_config(&turn, 0).expect("resume config"); @@ -2096,7 +2104,7 @@ mod tests { expected.model = Some(turn.model_info.slug.clone()); expected.model_provider = turn.provider.clone(); expected.model_reasoning_effort = turn.reasoning_effort; - expected.model_reasoning_summary = turn.reasoning_summary; + expected.model_reasoning_summary = Some(turn.reasoning_summary); expected.developer_instructions = turn.developer_instructions.clone(); expected.compact_prompt = turn.compact_prompt.clone(); expected.permissions.shell_environment_policy = turn.shell_environment_policy.clone(); @@ -2105,7 +2113,7 @@ mod tests { expected .permissions .approval_policy - .set(AskForApproval::Never) + .set(AskForApproval::OnRequest) .expect("approval policy set"); expected .permissions diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs index 134294d428..f9b444186e 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -12,10 +12,15 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; use codex_protocol::request_user_input::RequestUserInputArgs; -fn format_allowed_modes() -> String { +fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_input: bool) -> bool { + mode.allows_request_user_input() + || (default_mode_request_user_input && mode == ModeKind::Default) +} + +fn format_allowed_modes(default_mode_request_user_input: bool) -> String { let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES .into_iter() - .filter(|mode| mode.allows_request_user_input()) + .filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input)) .map(ModeKind::display_name) .collect(); @@ -27,8 +32,11 @@ fn format_allowed_modes() -> String { } } -pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option { - if mode.allows_request_user_input() { +pub(crate) fn request_user_input_unavailable_message( + mode: ModeKind, + default_mode_request_user_input: bool, +) -> Option { + if request_user_input_is_available(mode, default_mode_request_user_input) { None } else { let mode_name = mode.display_name(); @@ -38,14 +46,16 @@ pub(crate) fn request_user_input_unavailable_message(mode: ModeKind) -> Option String { - let allowed_modes = format_allowed_modes(); +pub(crate) fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String { + let allowed_modes = format_allowed_modes(default_mode_request_user_input); format!( "Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}." ) } -pub struct RequestUserInputHandler; +pub struct RequestUserInputHandler { + pub default_mode_request_user_input: bool, +} #[async_trait] impl ToolHandler for RequestUserInputHandler { @@ -72,7 +82,9 @@ impl ToolHandler for RequestUserInputHandler { }; let mode = session.collaboration_mode().await.mode; - if let Some(message) = request_user_input_unavailable_message(mode) { + if let Some(message) = + request_user_input_unavailable_message(mode, self.default_mode_request_user_input) + { return Err(FunctionCallError::RespondToModel(message)); } @@ -117,7 +129,7 @@ mod tests { use pretty_assertions::assert_eq; #[test] - fn request_user_input_mode_availability_is_plan_only() { + fn request_user_input_mode_availability_defaults_to_plan_only() { assert!(ModeKind::Plan.allows_request_user_input()); assert!(!ModeKind::Default.allows_request_user_input()); assert!(!ModeKind::Execute.allows_request_user_input()); @@ -125,27 +137,38 @@ mod tests { } #[test] - fn request_user_input_unavailable_messages_use_default_name_for_default_modes() { - assert_eq!(request_user_input_unavailable_message(ModeKind::Plan), None); + fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() { assert_eq!( - request_user_input_unavailable_message(ModeKind::Default), + request_user_input_unavailable_message(ModeKind::Plan, false), + None + ); + assert_eq!( + request_user_input_unavailable_message(ModeKind::Default, false), Some("request_user_input is unavailable in Default mode".to_string()) ); assert_eq!( - request_user_input_unavailable_message(ModeKind::Execute), + request_user_input_unavailable_message(ModeKind::Default, true), + None + ); + assert_eq!( + request_user_input_unavailable_message(ModeKind::Execute, false), Some("request_user_input is unavailable in Execute mode".to_string()) ); assert_eq!( - request_user_input_unavailable_message(ModeKind::PairProgramming), + request_user_input_unavailable_message(ModeKind::PairProgramming, false), Some("request_user_input is unavailable in Pair Programming mode".to_string()) ); } #[test] - fn request_user_input_tool_description_mentions_plan_only() { + fn request_user_input_tool_description_mentions_available_modes() { assert_eq!( - request_user_input_tool_description(), + request_user_input_tool_description(false), "Request user input for one to three short questions and wait for the response. This tool is only available in Plan mode.".to_string() ); + assert_eq!( + request_user_input_tool_description(true), + "Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string() + ); } } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 0a0bb53476..efc596ea36 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -14,8 +14,6 @@ use crate::function_tool::FunctionCallError; use crate::is_safe_command::is_known_safe_command; use crate::protocol::ExecCommandSource; use crate::shell::Shell; -use crate::skills::SKILL_APPROVAL_DECLINED_MESSAGE; -use crate::skills::ensure_skill_approval_for_command; use crate::skills::maybe_emit_implicit_skill_invocation; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; @@ -24,7 +22,8 @@ use crate::tools::events::ToolEmitter; use crate::tools::events::ToolEventCtx; use crate::tools::handlers::apply_patch::intercept_apply_patch; use crate::tools::handlers::normalize_and_validate_additional_permissions; -use crate::tools::handlers::parse_arguments; +use crate::tools::handlers::parse_arguments_with_base_path; +use crate::tools::handlers::resolve_workdir_base_path; use crate::tools::orchestrator::ToolOrchestrator; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; @@ -33,7 +32,7 @@ use crate::tools::runtimes::shell::ShellRuntime; use crate::tools::runtimes::shell::ShellRuntimeBackend; use crate::tools::sandboxing::ToolCtx; use crate::tools::spec::ShellCommandBackendConfig; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; pub struct ShellHandler; @@ -50,7 +49,7 @@ pub struct ShellCommandHandler { struct RunExecLikeArgs { tool_name: String, exec_params: ExecParams, - additional_permissions: Option, + additional_permissions: Option, prefix_rule: Option>, session: Arc, turn: Arc, @@ -68,8 +67,6 @@ impl ShellHandler { ) -> ExecParams { ExecParams { command: params.command.clone(), - original_command: shlex::try_join(params.command.iter().map(String::as_str)) - .unwrap_or_else(|_| params.command.join(" ")), cwd: turn_context.resolve_path(params.workdir.clone()), expiration: params.timeout_ms.into(), env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), @@ -120,7 +117,6 @@ impl ShellCommandHandler { Ok(ExecParams { command, - original_command: params.command.clone(), cwd: turn_context.resolve_path(params.workdir.clone()), expiration: params.timeout_ms.into(), env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), @@ -181,7 +177,9 @@ impl ToolHandler for ShellHandler { match payload { ToolPayload::Function { arguments } => { - let params: ShellToolCallParams = parse_arguments(&arguments)?; + let cwd = resolve_workdir_base_path(&arguments, turn.cwd.as_path())?; + let params: ShellToolCallParams = + parse_arguments_with_base_path(&arguments, cwd.as_path())?; let prefix_rule = params.prefix_rule.clone(); let exec_params = Self::to_exec_params(¶ms, turn.as_ref(), session.conversation_id); @@ -271,7 +269,16 @@ impl ToolHandler for ShellCommandHandler { ))); }; - let params: ShellCommandToolCallParams = parse_arguments(&arguments)?; + let cwd = resolve_workdir_base_path(&arguments, turn.cwd.as_path())?; + let params: ShellCommandToolCallParams = + parse_arguments_with_base_path(&arguments, cwd.as_path())?; + maybe_emit_implicit_skill_invocation( + session.as_ref(), + turn.as_ref(), + ¶ms.command, + params.workdir.as_deref(), + ) + .await; let prefix_rule = params.prefix_rule.clone(); let exec_params = Self::to_exec_params( ¶ms, @@ -348,28 +355,6 @@ impl ShellHandler { "approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}" ))); } - let original_command = exec_params.original_command.as_str(); - if !ensure_skill_approval_for_command( - session.as_ref(), - turn.as_ref(), - &call_id, - original_command, - exec_params.cwd.as_path(), - ) - .await - { - return Err(FunctionCallError::RespondToModel( - SKILL_APPROVAL_DECLINED_MESSAGE.to_string(), - )); - } - let workdir = exec_params.cwd.to_string_lossy().into_owned(); - maybe_emit_implicit_skill_invocation( - session.as_ref(), - turn.as_ref(), - original_command, - Some(workdir.as_str()), - ) - .await; // Intercept apply_patch if present. if let Some(output) = intercept_apply_patch( diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 9c5e3bc4b3..a04c5883c6 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -13,6 +13,8 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::apply_patch::intercept_apply_patch; use crate::tools::handlers::normalize_and_validate_additional_permissions; use crate::tools::handlers::parse_arguments; +use crate::tools::handlers::parse_arguments_with_base_path; +use crate::tools::handlers::resolve_workdir_base_path; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use crate::unified_exec::ExecCommandRequest; @@ -21,8 +23,8 @@ use crate::unified_exec::UnifiedExecProcessManager; use crate::unified_exec::UnifiedExecResponse; use crate::unified_exec::WriteStdinRequest; use async_trait::async_trait; -use codex_protocol::models::AdditionalPermissions; use codex_protocol::models::FunctionCallOutputBody; +use codex_protocol::models::PermissionProfile; use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; @@ -47,7 +49,7 @@ pub(crate) struct ExecCommandArgs { #[serde(default)] sandbox_permissions: SandboxPermissions, #[serde(default)] - additional_permissions: Option, + additional_permissions: Option, #[serde(default)] justification: Option, #[serde(default)] @@ -136,7 +138,9 @@ impl ToolHandler for UnifiedExecHandler { let response = match tool_name.as_str() { "exec_command" => { - let args: ExecCommandArgs = parse_arguments(&arguments)?; + let cwd = resolve_workdir_base_path(&arguments, context.turn.cwd.as_path())?; + let args: ExecCommandArgs = + parse_arguments_with_base_path(&arguments, cwd.as_path())?; maybe_emit_implicit_skill_invocation( session.as_ref(), turn.as_ref(), @@ -183,7 +187,7 @@ impl ToolHandler for UnifiedExecHandler { let workdir = workdir.filter(|value| !value.is_empty()); let workdir = workdir.map(|dir| context.turn.resolve_path(Some(dir))); - let cwd = workdir.clone().unwrap_or_else(|| context.turn.cwd.clone()); + let cwd = workdir.clone().unwrap_or(cwd); let normalized_additional_permissions = match normalize_and_validate_additional_permissions( request_permission_enabled, @@ -336,8 +340,15 @@ fn format_response(response: &UnifiedExecResponse) -> String { mod tests { use super::*; use crate::shell::default_user_shell; + use crate::tools::handlers::parse_arguments_with_base_path; + use crate::tools::handlers::resolve_workdir_base_path; + use codex_protocol::models::FileSystemPermissions; + use codex_protocol::models::PermissionProfile; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; + use std::fs; use std::sync::Arc; + use tempfile::tempdir; #[test] fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()> { @@ -420,4 +431,37 @@ mod tests { ); Ok(()) } + + #[test] + fn exec_command_args_resolve_relative_additional_permissions_against_workdir() + -> anyhow::Result<()> { + let cwd = tempdir()?; + let workdir = cwd.path().join("nested"); + fs::create_dir_all(&workdir)?; + let expected_write = workdir.join("relative-write.txt"); + let json = r#"{ + "cmd": "echo hello", + "workdir": "nested", + "additional_permissions": { + "file_system": { + "write": ["./relative-write.txt"] + } + } + }"#; + + let base_path = resolve_workdir_base_path(json, cwd.path())?; + let args: ExecCommandArgs = parse_arguments_with_base_path(json, base_path.as_path())?; + + assert_eq!( + args.additional_permissions, + Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![AbsolutePathBuf::try_from(expected_write)?]), + }), + ..Default::default() + }) + ); + Ok(()) + } } diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index ea05f736e7..9bf03acb00 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -8,7 +8,6 @@ use tokio::fs; use crate::function_tool::FunctionCallError; use crate::protocol::EventMsg; use crate::protocol::ViewImageToolCallEvent; -use crate::tools::context::ToolCallSource; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; @@ -16,7 +15,6 @@ use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use codex_protocol::models::ContentItem; -use codex_protocol::models::ResponseInputItem; use codex_protocol::models::local_image_content_items_with_label_number; pub struct ViewImageHandler; @@ -52,7 +50,6 @@ impl ToolHandler for ViewImageHandler { turn, payload, call_id, - source, .. } = invocation; @@ -85,24 +82,6 @@ impl ToolHandler for ViewImageHandler { let event_path = abs_path.clone(); let content = local_image_content_items_with_label_number(&abs_path, None); - if source == ToolCallSource::JsRepl - && content - .iter() - .any(|item| matches!(item, ContentItem::InputImage { .. })) - { - let input_item = ResponseInputItem::Message { - role: "user".to_string(), - content: content.clone(), - }; - if session - .inject_response_items(vec![input_item]) - .await - .is_err() - { - tracing::warn!("view_image could not find an active turn to attach image input"); - } - } - let content = content .into_iter() .map(|item| match item { diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index a5bd81049f..a234fd0eb8 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -9,6 +9,10 @@ use std::sync::Arc; use std::time::Duration; use codex_protocol::ThreadId; +use codex_protocol::models::ContentItem; +use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::ResponseInputItem; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -21,6 +25,8 @@ use tokio::sync::Mutex; use tokio::sync::Notify; use tokio::sync::OnceCell; use tokio_util::sync::CancellationToken; +use tracing::info; +use tracing::trace; use tracing::warn; use uuid::Uuid; @@ -36,6 +42,8 @@ use crate::sandboxing::SandboxPermissions; use crate::tools::ToolRouter; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::sandboxing::SandboxablePreference; +use crate::truncate::TruncationPolicy; +use crate::truncate::truncate_text; pub(crate) const JS_REPL_PRAGMA_PREFIX: &str = "// codex-js-repl:"; const KERNEL_SOURCE: &str = include_str!("kernel.js"); @@ -48,6 +56,7 @@ const JS_REPL_STDERR_TAIL_SEPARATOR: &str = " | "; const JS_REPL_EXEC_ID_LOG_LIMIT: usize = 8; const JS_REPL_MODEL_DIAG_STDERR_MAX_BYTES: usize = 1_024; const JS_REPL_MODEL_DIAG_ERROR_MAX_BYTES: usize = 256; +const JS_REPL_TOOL_RESPONSE_TEXT_PREVIEW_MAX_BYTES: usize = 512; /// Per-task js_repl handle stored on the turn context. pub(crate) struct JsReplHandle { @@ -95,6 +104,7 @@ pub struct JsReplArgs { #[derive(Clone, Debug)] pub struct JsExecResult { pub output: String, + pub content_items: Vec, } struct KernelState { @@ -116,10 +126,37 @@ struct ExecContext { #[derive(Default)] struct ExecToolCalls { in_flight: usize, + content_items: Vec, notify: Arc, cancel: CancellationToken, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::enum_variant_names)] +enum JsReplToolCallPayloadKind { + MessageContent, + FunctionText, + FunctionContentItems, + CustomText, + CustomContentItems, + McpResult, + McpErrorResult, + Error, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct JsReplToolCallResponseSummary { + response_type: Option, + payload_kind: Option, + payload_text_preview: Option, + payload_text_length: Option, + payload_item_count: Option, + text_item_count: Option, + image_item_count: Option, + structured_content_present: Option, + result_is_error: Option, +} + enum KernelStreamEnd { Shutdown, StdoutEof, @@ -335,6 +372,21 @@ impl JsReplManager { Some(state.cancel.clone()) } + async fn record_exec_tool_call_content_items( + exec_tool_calls: &Arc>>, + exec_id: &str, + content_items: Vec, + ) { + if content_items.is_empty() { + return; + } + + let mut calls = exec_tool_calls.lock().await; + if let Some(state) = calls.get_mut(exec_id) { + state.content_items.extend(content_items); + } + } + async fn finish_exec_tool_call( exec_tool_calls: &Arc>>, exec_id: &str, @@ -401,6 +453,205 @@ impl JsReplManager { } } + fn log_tool_call_response( + req: &RunToolRequest, + ok: bool, + summary: &JsReplToolCallResponseSummary, + response: Option<&JsonValue>, + error: Option<&str>, + ) { + info!( + exec_id = %req.exec_id, + tool_call_id = %req.id, + tool_name = %req.tool_name, + ok, + summary = ?summary, + "js_repl nested tool call completed" + ); + if let Some(response) = response { + trace!( + exec_id = %req.exec_id, + tool_call_id = %req.id, + tool_name = %req.tool_name, + response_json = %response, + "js_repl nested tool call raw response" + ); + } + if let Some(error) = error { + trace!( + exec_id = %req.exec_id, + tool_call_id = %req.id, + tool_name = %req.tool_name, + error = %error, + "js_repl nested tool call raw error" + ); + } + } + + fn summarize_text_payload( + response_type: Option<&str>, + payload_kind: JsReplToolCallPayloadKind, + text: &str, + ) -> JsReplToolCallResponseSummary { + JsReplToolCallResponseSummary { + response_type: response_type.map(str::to_owned), + payload_kind: Some(payload_kind), + payload_text_preview: (!text.is_empty()).then(|| { + truncate_text( + text, + TruncationPolicy::Bytes(JS_REPL_TOOL_RESPONSE_TEXT_PREVIEW_MAX_BYTES), + ) + }), + payload_text_length: Some(text.len()), + ..Default::default() + } + } + + fn summarize_function_output_payload( + response_type: &str, + payload_kind: JsReplToolCallPayloadKind, + output: &FunctionCallOutputPayload, + ) -> JsReplToolCallResponseSummary { + let (payload_item_count, text_item_count, image_item_count) = + if let Some(items) = output.content_items() { + let text_item_count = items + .iter() + .filter(|item| matches!(item, FunctionCallOutputContentItem::InputText { .. })) + .count(); + let image_item_count = items.len().saturating_sub(text_item_count); + ( + Some(items.len()), + Some(text_item_count), + Some(image_item_count), + ) + } else { + (None, None, None) + }; + let payload_text = output.body.to_text(); + JsReplToolCallResponseSummary { + response_type: Some(response_type.to_string()), + payload_kind: Some(payload_kind), + payload_text_preview: payload_text.as_deref().and_then(|text| { + (!text.is_empty()).then(|| { + truncate_text( + text, + TruncationPolicy::Bytes(JS_REPL_TOOL_RESPONSE_TEXT_PREVIEW_MAX_BYTES), + ) + }) + }), + payload_text_length: payload_text.as_ref().map(String::len), + payload_item_count, + text_item_count, + image_item_count, + ..Default::default() + } + } + + fn summarize_message_payload(content: &[ContentItem]) -> JsReplToolCallResponseSummary { + let text_item_count = content + .iter() + .filter(|item| { + matches!( + item, + ContentItem::InputText { .. } | ContentItem::OutputText { .. } + ) + }) + .count(); + let image_item_count = content.len().saturating_sub(text_item_count); + let payload_text = content + .iter() + .filter_map(|item| match item { + ContentItem::InputText { text } | ContentItem::OutputText { text } + if !text.trim().is_empty() => + { + Some(text.as_str()) + } + ContentItem::InputText { .. } + | ContentItem::InputImage { .. } + | ContentItem::OutputText { .. } => None, + }) + .collect::>(); + let payload_text = if payload_text.is_empty() { + None + } else { + Some(payload_text.join("\n")) + }; + JsReplToolCallResponseSummary { + response_type: Some("message".to_string()), + payload_kind: Some(JsReplToolCallPayloadKind::MessageContent), + payload_text_preview: payload_text.as_deref().and_then(|text| { + (!text.is_empty()).then(|| { + truncate_text( + text, + TruncationPolicy::Bytes(JS_REPL_TOOL_RESPONSE_TEXT_PREVIEW_MAX_BYTES), + ) + }) + }), + payload_text_length: payload_text.as_ref().map(String::len), + payload_item_count: Some(content.len()), + text_item_count: Some(text_item_count), + image_item_count: Some(image_item_count), + ..Default::default() + } + } + + fn summarize_tool_call_response(response: &ResponseInputItem) -> JsReplToolCallResponseSummary { + match response { + ResponseInputItem::Message { content, .. } => Self::summarize_message_payload(content), + ResponseInputItem::FunctionCallOutput { output, .. } => { + let payload_kind = if output.content_items().is_some() { + JsReplToolCallPayloadKind::FunctionContentItems + } else { + JsReplToolCallPayloadKind::FunctionText + }; + Self::summarize_function_output_payload( + "function_call_output", + payload_kind, + output, + ) + } + ResponseInputItem::CustomToolCallOutput { output, .. } => { + let payload_kind = if output.content_items().is_some() { + JsReplToolCallPayloadKind::CustomContentItems + } else { + JsReplToolCallPayloadKind::CustomText + }; + Self::summarize_function_output_payload( + "custom_tool_call_output", + payload_kind, + output, + ) + } + ResponseInputItem::McpToolCallOutput { result, .. } => match result { + Ok(result) => { + let output = FunctionCallOutputPayload::from(result); + let mut summary = Self::summarize_function_output_payload( + "mcp_tool_call_output", + JsReplToolCallPayloadKind::McpResult, + &output, + ); + summary.payload_item_count = Some(result.content.len()); + summary.structured_content_present = Some(result.structured_content.is_some()); + summary.result_is_error = Some(result.is_error.unwrap_or(false)); + summary + } + Err(error) => { + let mut summary = Self::summarize_text_payload( + Some("mcp_tool_call_output"), + JsReplToolCallPayloadKind::McpErrorResult, + error, + ); + summary.result_is_error = Some(true); + summary + } + }, + } + } + + fn summarize_tool_call_error(error: &str) -> JsReplToolCallResponseSummary { + Self::summarize_text_payload(None, JsReplToolCallPayloadKind::Error, error) + } + pub async fn reset(&self) -> Result<(), FunctionCallError> { let _permit = self.exec_lock.clone().acquire_owned().await.map_err(|_| { FunctionCallError::RespondToModel("js_repl execution unavailable".to_string()) @@ -543,7 +794,13 @@ impl JsReplManager { }; match response { - ExecResultMessage::Ok { output } => Ok(JsExecResult { output }), + ExecResultMessage::Ok { content_items } => { + let (output, content_items) = split_exec_result_content_items(content_items); + Ok(JsExecResult { + output, + content_items, + }) + } ExecResultMessage::Err { message } => Err(FunctionCallError::RespondToModel(message)), } } @@ -553,10 +810,7 @@ impl JsReplManager { turn: Arc, thread_id: Option, ) -> Result { - let node_path = resolve_node(self.node_path.as_deref()).ok_or_else(|| { - "Node runtime not found; install Node or set CODEX_JS_REPL_NODE_PATH".to_string() - })?; - ensure_node_version(&node_path).await?; + let node_path = resolve_compatible_node(self.node_path.as_deref()).await?; let kernel_path = self .write_kernel_script() @@ -613,6 +867,8 @@ impl JsReplManager { enforce_managed_network: has_managed_network_requirements, network: None, sandbox_policy_cwd: &turn.cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), use_linux_sandbox_bwrap: turn .features @@ -848,10 +1104,22 @@ impl JsReplManager { error, } => { JsReplManager::wait_for_exec_tool_calls_map(&exec_tool_calls, &id).await; + let content_items = { + let calls = exec_tool_calls.lock().await; + calls + .get(&id) + .map(|state| state.content_items.clone()) + .unwrap_or_default() + }; let mut pending = pending_execs.lock().await; if let Some(tx) = pending.remove(&id) { let payload = if ok { - ExecResultMessage::Ok { output } + ExecResultMessage::Ok { + content_items: build_exec_result_content_items( + output, + content_items, + ), + } } else { ExecResultMessage::Err { message: error @@ -908,7 +1176,11 @@ impl JsReplManager { response: None, error: Some("js_repl execution reset".to_string()), }, - result = JsReplManager::run_tool_request(ctx, req) => result, + result = JsReplManager::run_tool_request( + ctx, + req, + Arc::clone(&exec_tool_calls_for_task), + ) => result, } } None => RunToolResult { @@ -1002,13 +1274,20 @@ impl JsReplManager { } } - async fn run_tool_request(exec: ExecContext, req: RunToolRequest) -> RunToolResult { + async fn run_tool_request( + exec: ExecContext, + req: RunToolRequest, + exec_tool_calls: Arc>>, + ) -> RunToolResult { if is_js_repl_internal_tool(&req.tool_name) { + let error = "js_repl cannot invoke itself".to_string(); + let summary = Self::summarize_tool_call_error(&error); + Self::log_tool_call_response(&req, false, &summary, None, Some(&error)); return RunToolResult { id: req.id, ok: false, response: None, - error: Some("js_repl cannot invoke itself".to_string()), + error: Some(error), }; } @@ -1050,42 +1329,72 @@ impl JsReplManager { } }; + let tool_name = req.tool_name.clone(); let call = crate::tools::router::ToolCall { - tool_name: req.tool_name, + tool_name: tool_name.clone(), call_id: req.id.clone(), payload, }; + let session = Arc::clone(&exec.session); + let turn = Arc::clone(&exec.turn); + let tracker = Arc::clone(&exec.tracker); + match router .dispatch_tool_call( - exec.session, - exec.turn, - exec.tracker, + session.clone(), + turn, + tracker, call, crate::tools::router::ToolCallSource::JsRepl, ) .await { - Ok(response) => match serde_json::to_value(response) { - Ok(value) => RunToolResult { - id: req.id, - ok: true, - response: Some(value), - error: None, - }, - Err(err) => RunToolResult { + Ok(response) => { + if let Some(items) = response_content_items(&response) { + Self::record_exec_tool_call_content_items( + &exec_tool_calls, + &req.exec_id, + items, + ) + .await; + } + + let summary = Self::summarize_tool_call_response(&response); + match serde_json::to_value(response) { + Ok(value) => { + Self::log_tool_call_response(&req, true, &summary, Some(&value), None); + RunToolResult { + id: req.id, + ok: true, + response: Some(value), + error: None, + } + } + Err(err) => { + let error = format!("failed to serialize tool output: {err}"); + let summary = Self::summarize_tool_call_error(&error); + Self::log_tool_call_response(&req, false, &summary, None, Some(&error)); + RunToolResult { + id: req.id, + ok: false, + response: None, + error: Some(error), + } + } + } + } + Err(err) => { + let error = err.to_string(); + let summary = Self::summarize_tool_call_error(&error); + Self::log_tool_call_response(&req, false, &summary, None, Some(&error)); + RunToolResult { id: req.id, ok: false, response: None, - error: Some(format!("failed to serialize tool output: {err}")), - }, - }, - Err(err) => RunToolResult { - id: req.id, - ok: false, - response: None, - error: Some(err.to_string()), - }, + error: Some(error), + } + } } } @@ -1123,6 +1432,50 @@ impl JsReplManager { } } +fn response_content_items( + response: &ResponseInputItem, +) -> Option> { + match response { + ResponseInputItem::FunctionCallOutput { output, .. } + | ResponseInputItem::CustomToolCallOutput { output, .. } => output + .content_items() + .map(<[FunctionCallOutputContentItem]>::to_vec), + ResponseInputItem::McpToolCallOutput { result, .. } => match result { + Ok(result) => FunctionCallOutputPayload::from(result) + .content_items() + .map(<[FunctionCallOutputContentItem]>::to_vec), + Err(_) => None, + }, + ResponseInputItem::Message { .. } => None, + } +} + +fn build_exec_result_content_items( + output: String, + content_items: Vec, +) -> Vec { + let mut all_content_items = Vec::with_capacity(content_items.len() + 1); + all_content_items.push(FunctionCallOutputContentItem::InputText { text: output }); + all_content_items.extend(content_items); + all_content_items +} + +fn split_exec_result_content_items( + mut content_items: Vec, +) -> (String, Vec) { + match content_items.first() { + Some(FunctionCallOutputContentItem::InputText { .. }) => { + let FunctionCallOutputContentItem::InputText { text } = content_items.remove(0) else { + unreachable!("first content item should be input_text"); + }; + (text, content_items) + } + Some(FunctionCallOutputContentItem::InputImage { .. }) | None => { + (String::new(), content_items) + } + } +} + fn is_freeform_tool(specs: &[ToolSpec], name: &str) -> bool { specs .iter() @@ -1178,8 +1531,12 @@ struct RunToolResult { #[derive(Debug)] enum ExecResultMessage { - Ok { output: String }, - Err { message: String }, + Ok { + content_items: Vec, + }, + Err { + message: String, + }, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -1276,6 +1633,14 @@ async fn ensure_node_version(node_path: &Path) -> Result<(), String> { Ok(()) } +pub(crate) async fn resolve_compatible_node(config_path: Option<&Path>) -> Result { + let node_path = resolve_node(config_path).ok_or_else(|| { + "Node runtime not found; install Node or set CODEX_JS_REPL_NODE_PATH".to_string() + })?; + ensure_node_version(&node_path).await?; + Ok(node_path) +} + pub(crate) fn resolve_node(config_path: Option<&Path>) -> Option { if let Some(path) = std::env::var_os("CODEX_JS_REPL_NODE_PATH") { let p = PathBuf::from(path); @@ -1301,10 +1666,16 @@ pub(crate) fn resolve_node(config_path: Option<&Path>) -> Option { mod tests { use super::*; use crate::codex::make_session_and_context; + use crate::codex::make_session_and_context_with_dynamic_tools_and_rx; use crate::protocol::AskForApproval; + use crate::protocol::EventMsg; use crate::protocol::SandboxPolicy; use crate::turn_diff_tracker::TurnDiffTracker; - use codex_protocol::models::ContentItem; + use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; + use codex_protocol::dynamic_tools::DynamicToolResponse; + use codex_protocol::dynamic_tools::DynamicToolSpec; + use codex_protocol::models::FunctionCallOutputContentItem; + use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use codex_protocol::openai_models::InputModality; use pretty_assertions::assert_eq; @@ -1522,6 +1893,84 @@ mod tests { ); } + #[test] + fn summarize_tool_call_response_for_multimodal_function_output() { + let response = ResponseInputItem::FunctionCallOutput { + call_id: "call-1".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,abcd".to_string(), + }, + ]), + }; + + let actual = JsReplManager::summarize_tool_call_response(&response); + + assert_eq!( + actual, + JsReplToolCallResponseSummary { + response_type: Some("function_call_output".to_string()), + payload_kind: Some(JsReplToolCallPayloadKind::FunctionContentItems), + payload_text_preview: None, + payload_text_length: None, + payload_item_count: Some(1), + text_item_count: Some(0), + image_item_count: Some(1), + structured_content_present: None, + result_is_error: None, + } + ); + } + + #[test] + fn summarize_tool_call_response_for_multimodal_custom_output() { + let response = ResponseInputItem::CustomToolCallOutput { + call_id: "call-1".to_string(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,abcd".to_string(), + }, + ]), + }; + + let actual = JsReplManager::summarize_tool_call_response(&response); + + assert_eq!( + actual, + JsReplToolCallResponseSummary { + response_type: Some("custom_tool_call_output".to_string()), + payload_kind: Some(JsReplToolCallPayloadKind::CustomContentItems), + payload_text_preview: None, + payload_text_length: None, + payload_item_count: Some(1), + text_item_count: Some(0), + image_item_count: Some(1), + structured_content_present: None, + result_is_error: None, + } + ); + } + + #[test] + fn summarize_tool_call_error_marks_error_payload() { + let actual = JsReplManager::summarize_tool_call_error("tool failed"); + + assert_eq!( + actual, + JsReplToolCallResponseSummary { + response_type: None, + payload_kind: Some(JsReplToolCallPayloadKind::Error), + payload_text_preview: Some("tool failed".to_string()), + payload_text_length: Some("tool failed".len()), + payload_item_count: None, + text_item_count: None, + image_item_count: None, + structured_content_present: None, + result_is_error: None, + } + ); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn reset_clears_inflight_exec_tool_calls_without_waiting() { let manager = JsReplManager::new(None, Vec::new()) @@ -1586,23 +2035,11 @@ mod tests { } async fn can_run_js_repl_runtime_tests() -> bool { - if std::env::var_os("CODEX_SANDBOX").is_some() { - return false; - } - let Some(node_path) = resolve_node(None) else { - return false; - }; - let required = match required_node_version() { - Ok(v) => v, - Err(_) => return false, - }; - let found = match read_node_version(&node_path).await { - Ok(v) => v, - Err(_) => return false, - }; - found >= required + // These white-box runtime tests are required on macOS. Linux relies on + // the codex-linux-sandbox arg0 dispatch path, which is exercised in + // integration tests instead. + cfg!(target_os = "macos") } - fn write_js_repl_test_package(base: &Path, name: &str, value: &str) -> anyhow::Result<()> { let pkg_dir = base.join("node_modules").join(name); fs::create_dir_all(&pkg_dir)?; @@ -1619,47 +2056,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn js_repl_persists_top_level_bindings_and_supports_tla() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await { - return Ok(()); - } - - let (session, turn) = make_session_and_context().await; - let session = Arc::new(session); - let turn = Arc::new(turn); - let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); - let manager = turn.js_repl.manager().await?; - - let first = manager - .execute( - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - JsReplArgs { - code: "let x = await Promise.resolve(41); console.log(x);".to_string(), - timeout_ms: Some(10_000), - }, - ) - .await?; - assert!(first.output.contains("41")); - - let second = manager - .execute( - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - JsReplArgs { - code: "console.log(x + 1);".to_string(), - timeout_ms: Some(10_000), - }, - ) - .await?; - - assert!(second.output.contains("42")); - Ok(()) - } - #[tokio::test] async fn js_repl_timeout_does_not_deadlock() -> anyhow::Result<()> { if !can_run_js_repl_runtime_tests().await { @@ -1915,108 +2311,9 @@ mod tests { Ok(()) } - #[tokio::test] - async fn js_repl_can_call_tools() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await { - return Ok(()); - } - - let (session, mut turn) = make_session_and_context().await; - turn.approval_policy - .set(AskForApproval::Never) - .expect("test setup should allow updating approval policy"); - turn.sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); - - let session = Arc::new(session); - let turn = Arc::new(turn); - let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); - let manager = turn.js_repl.manager().await?; - - let shell = manager - .execute( - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - JsReplArgs { - code: "const shellOut = await codex.tool(\"shell_command\", { command: \"printf js_repl_shell_ok\" }); console.log(JSON.stringify(shellOut));".to_string(), - timeout_ms: Some(15_000), - }, - ) - .await?; - assert!(shell.output.contains("js_repl_shell_ok")); - - let tool = manager - .execute( - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - JsReplArgs { - code: "const toolOut = await codex.tool(\"list_mcp_resources\", {}); console.log(toolOut.type);".to_string(), - timeout_ms: Some(15_000), - }, - ) - .await?; - assert!(tool.output.contains("function_call_output")); - Ok(()) - } - - #[tokio::test] - async fn js_repl_tool_call_rejects_recursive_js_repl_invocation() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await { - return Ok(()); - } - - let (session, mut turn) = make_session_and_context().await; - turn.approval_policy - .set(AskForApproval::Never) - .expect("test setup should allow updating approval policy"); - turn.sandbox_policy - .set(SandboxPolicy::DangerFullAccess) - .expect("test setup should allow updating sandbox policy"); - - let session = Arc::new(session); - let turn = Arc::new(turn); - let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); - let manager = turn.js_repl.manager().await?; - - let result = manager - .execute( - session, - turn, - tracker, - JsReplArgs { - code: r#" -try { - await codex.tool("js_repl", "console.log('recursive')"); - console.log("unexpected-success"); -} catch (err) { - console.log(String(err)); -} -"# - .to_string(), - timeout_ms: Some(15_000), - }, - ) - .await?; - - assert!( - result.output.contains("js_repl cannot invoke itself"), - "expected recursion guard message, got output: {}", - result.output - ); - assert!( - !result.output.contains("unexpected-success"), - "recursive js_repl tool call unexpectedly succeeded: {}", - result.output - ); - Ok(()) - } - #[tokio::test] async fn js_repl_waits_for_unawaited_tool_calls_before_completion() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await || cfg!(windows) { + if !can_run_js_repl_runtime_tests().await { return Ok(()); } @@ -2114,83 +2411,117 @@ console.log(out.output?.body?.text ?? ""); ) .await?; assert!(result.output.contains("function_call_output")); - - let pending_input = session.get_pending_input().await; - let image_url = pending_input - .iter() - .find_map(|item| match item { - ResponseInputItem::Message { content, .. } => { - content.iter().find_map(|content_item| match content_item { - ContentItem::InputImage { image_url } => Some(image_url.as_str()), - _ => None, - }) - } - _ => None, - }) - .expect("view_image should inject an input_image message for the active turn"); - assert!(image_url.starts_with("data:image/png;base64,")); - - Ok(()) - } - - #[tokio::test] - async fn js_repl_does_not_expose_process_global() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await { - return Ok(()); - } - - let (session, turn) = make_session_and_context().await; - let session = Arc::new(session); - let turn = Arc::new(turn); - let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); - let manager = turn.js_repl.manager().await?; - - let result = manager - .execute( - session, - turn, - tracker, - JsReplArgs { - code: "console.log(typeof process);".to_string(), - timeout_ms: Some(10_000), - }, - ) - .await?; - assert!(result.output.contains("undefined")); - Ok(()) - } - - #[tokio::test] - async fn js_repl_blocks_sensitive_builtin_imports() -> anyhow::Result<()> { - if !can_run_js_repl_runtime_tests().await { - return Ok(()); - } - - let (session, turn) = make_session_and_context().await; - let session = Arc::new(session); - let turn = Arc::new(turn); - let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); - let manager = turn.js_repl.manager().await?; - - let err = manager - .execute( - session, - turn, - tracker, - JsReplArgs { - code: "await import(\"node:process\");".to_string(), - timeout_ms: Some(10_000), - }, - ) - .await - .expect_err("node:process import should be blocked"); - assert!( - err.to_string() - .contains("Importing module \"node:process\" is not allowed in js_repl") + assert_eq!( + result.content_items.as_slice(), + [FunctionCallOutputContentItem::InputImage { + image_url: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" + .to_string(), + }] + .as_slice() ); + let [FunctionCallOutputContentItem::InputImage { image_url }] = + result.content_items.as_slice() + else { + panic!("view_image should return exactly one input_image content item"); + }; + assert!(image_url.starts_with("data:image/png;base64,")); + assert!(session.get_pending_input().await.is_empty()); + Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn js_repl_can_attach_image_via_dynamic_tool_with_mixed_content() -> anyhow::Result<()> { + if !can_run_js_repl_runtime_tests().await { + return Ok(()); + } + + let (session, turn, rx_event) = + make_session_and_context_with_dynamic_tools_and_rx(vec![DynamicToolSpec { + name: "inline_image".to_string(), + description: "Returns inline text and image content.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": {}, + "additionalProperties": false + }), + }]) + .await; + if !turn + .model_info + .input_modalities + .contains(&InputModality::Image) + { + return Ok(()); + } + + *session.active_turn.lock().await = Some(crate::state::ActiveTurn::default()); + + let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::default())); + let manager = turn.js_repl.manager().await?; + let code = r#" +const out = await codex.tool("inline_image", {}); +console.log(out.type); +"#; + let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; + + let session_for_response = Arc::clone(&session); + let response_watcher = async move { + loop { + let event = tokio::time::timeout(Duration::from_secs(2), rx_event.recv()).await??; + if let EventMsg::DynamicToolCallRequest(request) = event.msg { + session_for_response + .notify_dynamic_tool_response( + &request.call_id, + DynamicToolResponse { + content_items: vec![ + DynamicToolCallOutputContentItem::InputText { + text: "inline image note".to_string(), + }, + DynamicToolCallOutputContentItem::InputImage { + image_url: image_url.to_string(), + }, + ], + success: true, + }, + ) + .await; + return Ok::<(), anyhow::Error>(()); + } + } + }; + + let (result, response_watcher_result) = tokio::join!( + manager.execute( + Arc::clone(&session), + Arc::clone(&turn), + tracker, + JsReplArgs { + code: code.to_string(), + timeout_ms: Some(15_000), + }, + ), + response_watcher, + ); + response_watcher_result?; + let result = result?; + assert!(result.output.contains("function_call_output")); + assert_eq!( + result.content_items, + vec![ + FunctionCallOutputContentItem::InputText { + text: "inline image note".to_string(), + }, + FunctionCallOutputContentItem::InputImage { + image_url: image_url.to_string(), + }, + ] + ); + assert!(session.get_pending_input().await.is_empty()); + + Ok(()) + } #[tokio::test] async fn js_repl_prefers_env_node_module_dirs_over_config() -> anyhow::Result<()> { if !can_run_js_repl_runtime_tests().await { diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index ca31ec4d4c..aeef59833d 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -331,6 +331,7 @@ impl NetworkApprovalService { protocol, }; + let available_decisions = None; let approval_decision = session .request_command_approval( turn_context.as_ref(), @@ -342,6 +343,7 @@ impl NetworkApprovalService { Some(network_approval_context.clone()), None, None, + available_decisions, ) .await; diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index 04d505c584..5f2fc89e5f 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -117,7 +117,10 @@ impl ToolCallRuntime { match &call.payload { ToolPayload::Custom { .. } => ResponseInputItem::CustomToolCallOutput { call_id: call.call_id.clone(), - output: Self::abort_message(call, secs), + output: FunctionCallOutputPayload { + body: FunctionCallOutputBody::Text(Self::abort_message(call, secs)), + ..Default::default() + }, }, ToolPayload::Mcp { .. } => ResponseInputItem::McpToolCallOutput { call_id: call.call_id.clone(), diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 7a0be68df3..3c021cc582 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -379,6 +379,7 @@ async fn dispatch_after_tool_use_hook( .dispatch(HookPayload { session_id: session.conversation_id, cwd: turn.cwd.clone(), + client: turn.app_server_client_name.clone(), triggered_at: chrono::Utc::now(), hook_event: HookEvent::AfterToolUse { event: HookEventAfterToolUse { diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index d95a2c49a7..a55fb5fd5a 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -175,7 +175,6 @@ impl ToolRouter { call_id, tool_name, payload, - source, }; match self.registry.dispatch(invocation).await { @@ -198,7 +197,10 @@ impl ToolRouter { if payload_outputs_custom { ResponseInputItem::CustomToolCallOutput { call_id, - output: message, + output: codex_protocol::models::FunctionCallOutputPayload { + body: FunctionCallOutputBody::Text(message), + success: Some(false), + }, } } else { ResponseInputItem::FunctionCallOutput { diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 6dc20002df..51002f3ce9 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -9,8 +9,9 @@ use crate::path_utils; use crate::sandboxing::CommandSpec; use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; +use crate::skills::SkillMetadata; use crate::tools::sandboxing::ToolError; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use std::collections::HashMap; use std::path::Path; @@ -18,6 +19,14 @@ pub mod apply_patch; pub mod shell; pub mod unified_exec; +#[derive(Debug, Clone)] +pub(crate) struct ExecveSessionApproval { + /// If this execve session approval is associated with a skill script, this + /// field contains metadata about the skill. + #[cfg_attr(not(unix), allow(dead_code))] + pub skill: Option, +} + /// Shared helper to construct a CommandSpec from a tokenized command line. /// Validates that at least a program is present. pub(crate) fn build_command_spec( @@ -26,7 +35,7 @@ pub(crate) fn build_command_spec( env: &HashMap, expiration: ExecExpiration, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, justification: Option, ) -> Result { let (program, args) = command diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 0a5353755c..101d9309fd 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -31,7 +31,7 @@ use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::sandbox_override_for_first_attempt; use crate::tools::sandboxing::with_cached_approval; use codex_network_proxy::NetworkProxy; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; use futures::future::BoxFuture; use std::collections::HashMap; @@ -46,7 +46,7 @@ pub struct ShellRequest { pub explicit_env_overrides: HashMap, pub network: Option, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub justification: Option, pub exec_approval_requirement: ExecApprovalRequirement, } @@ -89,7 +89,7 @@ pub(crate) struct ApprovalKey { command: Vec, cwd: PathBuf, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, } impl ShellRuntime { @@ -150,6 +150,7 @@ impl Approvable for ShellRuntime { let call_id = ctx.call_id.to_string(); Box::pin(async move { with_cached_approval(&session.services, "shell", keys, move || async move { + let available_decisions = None; session .request_command_approval( turn, @@ -163,6 +164,7 @@ impl Approvable for ShellRuntime { .proposed_execpolicy_amendment() .cloned(), req.additional_permissions.clone(), + available_decisions, ) .await }) 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 811b1fdfe6..ae1df34ece 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -8,14 +8,19 @@ use crate::exec::is_likely_sandbox_denied; use crate::features::Feature; use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; +use crate::skills::SkillMetadata; +use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::runtimes::build_command_spec; use crate::tools::sandboxing::SandboxAttempt; +use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use codex_execpolicy::Decision; use codex_execpolicy::Policy; use codex_execpolicy::RuleMatch; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::MacOsSeatbeltProfileExtensions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::RejectConfig; @@ -23,15 +28,19 @@ use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_shell_command::bash::parse_shell_lc_plain_commands; use codex_shell_command::bash::parse_shell_lc_single_command_prefix; -use codex_shell_escalation::EscalateAction; use codex_shell_escalation::EscalateServer; +use codex_shell_escalation::EscalationDecision; +use codex_shell_escalation::EscalationExecution; +use codex_shell_escalation::EscalationPermissions; use codex_shell_escalation::EscalationPolicy; use codex_shell_escalation::ExecParams; use codex_shell_escalation::ExecResult; +use codex_shell_escalation::Permissions as EscalatedPermissions; +use codex_shell_escalation::PreparedExec; use codex_shell_escalation::ShellCommandExecutor; use codex_shell_escalation::Stopwatch; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -70,14 +79,9 @@ pub(super) async fn try_run_zsh_fork( let sandbox_exec_request = attempt .env_for(spec, req.network.as_ref()) .map_err(|err| ToolError::Codex(err.into()))?; - // Keep env/network/sandbox metadata from `attempt.env_for()`, but build the - // script from the original shell argv. `attempt.env_for()` may wrap the - // command with `sandbox-exec` on macOS, and passing those wrapper flags - // (`-p`, `-D...`) through zsh breaks the zsh-fork path before subcommand - // approval runs. let crate::sandboxing::ExecRequest { - command: _sandbox_command, - cwd: _sandbox_cwd, + command, + cwd: sandbox_cwd, env: sandbox_env, network: sandbox_network, expiration: _sandbox_expiration, @@ -88,7 +92,7 @@ pub(super) async fn try_run_zsh_fork( justification, arg0, } = sandbox_exec_request; - let ParsedShellCommand { script, login } = extract_shell_script(command)?; + let ParsedShellCommand { script, login } = extract_shell_script(&command)?; let effective_timeout = Duration::from_millis( req.timeout_ms .unwrap_or(crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS), @@ -97,6 +101,8 @@ pub(super) async fn try_run_zsh_fork( ctx.session.services.exec_policy.current().as_ref().clone(), )); let command_executor = CoreShellCommandExecutor { + command, + cwd: sandbox_cwd, sandbox_policy, sandbox, env: sandbox_env, @@ -105,16 +111,32 @@ pub(super) async fn try_run_zsh_fork( sandbox_permissions, justification, arg0, + sandbox_policy_cwd: ctx.turn.cwd.clone(), + macos_seatbelt_profile_extensions: ctx + .turn + .config + .permissions + .macos_seatbelt_profile_extensions + .clone(), + codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(), + use_linux_sandbox_bwrap: ctx.turn.features.enabled(Feature::UseLinuxSandboxBwrap), }; - + let main_execve_wrapper_exe = ctx + .session + .services + .main_execve_wrapper_exe + .clone() + .ok_or_else(|| { + ToolError::Rejected( + "zsh fork feature enabled, but execve wrapper is not configured".to_string(), + ) + })?; let exec_params = ExecParams { command: script, workdir: req.cwd.to_string_lossy().to_string(), timeout_ms: Some(effective_timeout.as_millis() as u64), login: Some(login), }; - let execve_wrapper = - shell_execve_wrapper().map_err(|err| ToolError::Rejected(format!("{err}")))?; // Note that Stopwatch starts immediately upon creation, so currently we try // to minimize the time between creating the Stopwatch and starting the @@ -129,14 +151,18 @@ pub(super) async fn try_run_zsh_fork( approval_policy: ctx.turn.approval_policy.value(), sandbox_policy: attempt.policy.clone(), sandbox_permissions: req.sandbox_permissions, + prompt_permissions: req.additional_permissions.clone(), stopwatch: stopwatch.clone(), }; - let escalate_server = - EscalateServer::new(shell_zsh_path.clone(), execve_wrapper, escalation_policy); + let escalate_server = EscalateServer::new( + shell_zsh_path.clone(), + main_execve_wrapper_exe, + escalation_policy, + ); let exec_result = escalate_server - .exec(exec_params, cancel_token, &command_executor) + .exec(exec_params, cancel_token, Arc::new(command_executor)) .await .map_err(|err| ToolError::Rejected(err.to_string()))?; @@ -151,9 +177,20 @@ struct CoreShellActionProvider { approval_policy: AskForApproval, sandbox_policy: SandboxPolicy, sandbox_permissions: SandboxPermissions, + prompt_permissions: Option, stopwatch: Stopwatch, } +#[allow(clippy::large_enum_variant)] +enum DecisionSource { + SkillScript { + skill: SkillMetadata, + }, + PrefixRule, + /// Often, this is `is_safe_command()`. + UnmatchedCommandFallback, +} + impl CoreShellActionProvider { fn decision_driven_by_policy(matched_rules: &[RuleMatch], decision: Decision) -> bool { matched_rules.iter().any(|rule_match| { @@ -162,13 +199,65 @@ impl CoreShellActionProvider { }) } + fn shell_request_escalation_execution( + sandbox_permissions: SandboxPermissions, + sandbox_policy: &SandboxPolicy, + additional_permissions: Option<&PermissionProfile>, + macos_seatbelt_profile_extensions: Option<&MacOsSeatbeltProfileExtensions>, + ) -> EscalationExecution { + match sandbox_permissions { + SandboxPermissions::UseDefault => EscalationExecution::TurnDefault, + SandboxPermissions::RequireEscalated => EscalationExecution::Unsandboxed, + SandboxPermissions::WithAdditionalPermissions => additional_permissions + .map(|_| { + // Shell request additional permissions were already normalized and + // merged into the first-attempt sandbox policy. + EscalationExecution::Permissions(EscalationPermissions::Permissions( + EscalatedPermissions { + sandbox_policy: sandbox_policy.clone(), + macos_seatbelt_profile_extensions: macos_seatbelt_profile_extensions + .cloned(), + }, + )) + }) + .unwrap_or(EscalationExecution::TurnDefault), + } + } + + fn skill_escalation_execution(skill: &SkillMetadata) -> EscalationExecution { + skill + .permissions + .as_ref() + .map(|permissions| { + EscalationExecution::Permissions(EscalationPermissions::Permissions( + EscalatedPermissions { + sandbox_policy: permissions.sandbox_policy.get().clone(), + macos_seatbelt_profile_extensions: permissions + .macos_seatbelt_profile_extensions + .clone(), + }, + )) + }) + .or_else(|| { + skill + .permission_profile + .clone() + .map(EscalationPermissions::PermissionProfile) + .map(EscalationExecution::Permissions) + }) + .unwrap_or(EscalationExecution::TurnDefault) + } + async fn prompt( &self, - command: &[String], - workdir: &Path, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, stopwatch: &Stopwatch, + additional_permissions: Option, + decision_source: &DecisionSource, ) -> anyhow::Result { - let command = command.to_vec(); + let command = join_program_and_argv(program, argv); let workdir = workdir.to_path_buf(); let session = self.session.clone(); let turn = self.turn.clone(); @@ -176,6 +265,20 @@ impl CoreShellActionProvider { let approval_id = Some(Uuid::new_v4().to_string()); Ok(stopwatch .pause_for(async move { + let available_decisions = vec![ + Some(ReviewDecision::Approved), + // Currently, ApprovedForSession is only honored for skills, + // so only offer it for skill script approvals. + if matches!(decision_source, DecisionSource::SkillScript { .. }) { + Some(ReviewDecision::ApprovedForSession) + } else { + None + }, + Some(ReviewDecision::Abort), + ] + .into_iter() + .flatten() + .collect(); session .request_command_approval( &turn, @@ -186,25 +289,211 @@ impl CoreShellActionProvider { None, None, None, - None, + additional_permissions, + Some(available_decisions), ) .await }) .await) } + + /// Because we should be intercepting execve(2) calls, `program` should be + /// an absolute path. The idea is that we check to see whether it matches + /// any skills. + async fn find_skill(&self, program: &AbsolutePathBuf) -> Option { + let force_reload = false; + let skills_outcome = self + .session + .services + .skills_manager + .skills_for_cwd(&self.turn.cwd, force_reload) + .await; + + let program_path = program.as_path(); + for skill in skills_outcome.skills { + // We intentionally ignore "enabled" status here for now. + let Some(skill_root) = skill.path_to_skills_md.parent() else { + continue; + }; + if program_path.starts_with(skill_root.join("scripts")) { + return Some(skill); + } + } + + None + } + + #[allow(clippy::too_many_arguments)] + async fn process_decision( + &self, + decision: Decision, + needs_escalation: bool, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, + prompt_permissions: Option, + escalation_execution: EscalationExecution, + decision_source: DecisionSource, + ) -> anyhow::Result { + let action = match decision { + Decision::Forbidden => { + EscalationDecision::deny(Some("Execution forbidden by policy".to_string())) + } + Decision::Prompt => { + if matches!( + self.approval_policy, + AskForApproval::Never + | AskForApproval::Reject(RejectConfig { rules: true, .. }) + ) { + EscalationDecision::deny(Some("Execution forbidden by policy".to_string())) + } else { + match self + .prompt( + program, + argv, + workdir, + &self.stopwatch, + prompt_permissions, + &decision_source, + ) + .await? + { + ReviewDecision::Approved + | ReviewDecision::ApprovedExecpolicyAmendment { .. } => { + if needs_escalation { + EscalationDecision::escalate(escalation_execution.clone()) + } else { + EscalationDecision::run() + } + } + ReviewDecision::ApprovedForSession => { + // Currently, we only add session approvals for + // skill scripts because we are storing only the + // `program` whereas prefix rules may be restricted by a longer prefix. + if let DecisionSource::SkillScript { skill } = decision_source { + tracing::debug!( + "Adding session approval for {program:?} due to user approval of skill script {skill:?}" + ); + self.session + .services + .execve_session_approvals + .write() + .await + .insert( + program.clone(), + ExecveSessionApproval { + skill: Some(skill.clone()), + }, + ); + } + + if needs_escalation { + EscalationDecision::escalate(escalation_execution.clone()) + } else { + EscalationDecision::run() + } + } + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => match network_policy_amendment.action { + NetworkPolicyRuleAction::Allow => { + if needs_escalation { + EscalationDecision::escalate(escalation_execution.clone()) + } else { + EscalationDecision::run() + } + } + NetworkPolicyRuleAction::Deny => { + EscalationDecision::deny(Some("User denied execution".to_string())) + } + }, + ReviewDecision::Denied => { + EscalationDecision::deny(Some("User denied execution".to_string())) + } + ReviewDecision::Abort => { + EscalationDecision::deny(Some("User cancelled execution".to_string())) + } + } + } + } + Decision::Allow => { + if needs_escalation { + EscalationDecision::escalate(escalation_execution) + } else { + EscalationDecision::run() + } + } + }; + tracing::debug!( + "Policy decision for command {program:?} is {decision:?}, leading to escalation action {action:?}", + ); + Ok(action) + } } #[async_trait::async_trait] impl EscalationPolicy for CoreShellActionProvider { async fn determine_action( &self, - file: &Path, + program: &AbsolutePathBuf, argv: &[String], - workdir: &Path, - ) -> anyhow::Result { - let command = std::iter::once(file.to_string_lossy().to_string()) - .chain(argv.iter().cloned()) - .collect::>(); + workdir: &AbsolutePathBuf, + ) -> anyhow::Result { + tracing::debug!( + "Determining escalation action for command {program:?} with args {argv:?} in {workdir:?}" + ); + + // Check to see whether `program` has an existing entry in + // `execve_session_approvals`. If so, we can skip policy checks and user + // prompts and go straight to allowing execution. + let approval = { + self.session + .services + .execve_session_approvals + .read() + .await + .get(program) + .cloned() + }; + if let Some(approval) = approval { + tracing::debug!( + "Found session approval for {program:?}, allowing execution without further checks" + ); + let execution = approval + .skill + .as_ref() + .map(Self::skill_escalation_execution) + .unwrap_or(EscalationExecution::TurnDefault); + + return Ok(EscalationDecision::escalate(execution)); + } + + // In the usual case, the execve wrapper reports the command being + // executed in `program`, so a direct skill lookup is sufficient. + if let Some(skill) = self.find_skill(program).await { + // For now, we always prompt for scripts that look like they belong + // to skills, which means we ignore exec policy rules for those + // scripts. + tracing::debug!("Matched {program:?} to skill {skill:?}, prompting for approval"); + let needs_escalation = true; + let decision_source = DecisionSource::SkillScript { + skill: skill.clone(), + }; + return self + .process_decision( + Decision::Prompt, + needs_escalation, + program, + argv, + workdir, + skill.permission_profile.clone(), + Self::skill_escalation_execution(&skill), + decision_source, + ) + .await; + } + + let command = join_program_and_argv(program, argv); let (commands, used_complex_parsing) = if let Some(commands) = parse_shell_lc_plain_commands(&command) { (commands, false) @@ -234,59 +523,38 @@ impl EscalationPolicy for CoreShellActionProvider { let needs_escalation = self.sandbox_permissions.requires_escalated_permissions() || decision_driven_by_policy; - Ok(match evaluation.decision { - Decision::Forbidden => EscalateAction::Deny { - reason: Some("Execution forbidden by policy".to_string()), - }, - Decision::Prompt => { - if matches!( - self.approval_policy, - AskForApproval::Never - | AskForApproval::Reject(RejectConfig { rules: true, .. }) - ) { - EscalateAction::Deny { - reason: Some("Execution forbidden by policy".to_string()), - } - } else { - match self.prompt(&command, workdir, &self.stopwatch).await? { - ReviewDecision::Approved - | ReviewDecision::ApprovedExecpolicyAmendment { .. } - | ReviewDecision::ApprovedForSession => { - if needs_escalation { - EscalateAction::Escalate - } else { - EscalateAction::Run - } - } - ReviewDecision::NetworkPolicyAmendment { - network_policy_amendment, - } => match network_policy_amendment.action { - NetworkPolicyRuleAction::Allow => { - if needs_escalation { - EscalateAction::Escalate - } else { - EscalateAction::Run - } - } - NetworkPolicyRuleAction::Deny => EscalateAction::Deny { - reason: Some("User denied execution".to_string()), - }, - }, - ReviewDecision::Denied => EscalateAction::Deny { - reason: Some("User denied execution".to_string()), - }, - ReviewDecision::Abort => EscalateAction::Deny { - reason: Some("User cancelled execution".to_string()), - }, - } - } - } - Decision::Allow => EscalateAction::Escalate, - }) + let decision_source = if decision_driven_by_policy { + DecisionSource::PrefixRule + } else { + DecisionSource::UnmatchedCommandFallback + }; + let escalation_execution = Self::shell_request_escalation_execution( + self.sandbox_permissions, + &self.sandbox_policy, + self.prompt_permissions.as_ref(), + self.turn + .config + .permissions + .macos_seatbelt_profile_extensions + .as_ref(), + ); + self.process_decision( + evaluation.decision, + needs_escalation, + program, + argv, + workdir, + self.prompt_permissions.clone(), + escalation_execution, + decision_source, + ) + .await } } struct CoreShellCommandExecutor { + command: Vec, + cwd: PathBuf, sandbox_policy: SandboxPolicy, sandbox: SandboxType, env: HashMap, @@ -295,14 +563,18 @@ struct CoreShellCommandExecutor { sandbox_permissions: SandboxPermissions, justification: Option, arg0: Option, + sandbox_policy_cwd: PathBuf, + macos_seatbelt_profile_extensions: Option, + codex_linux_sandbox_exe: Option, + use_linux_sandbox_bwrap: bool, } #[async_trait::async_trait] impl ShellCommandExecutor for CoreShellCommandExecutor { async fn run( &self, - command: Vec, - cwd: PathBuf, + _command: Vec, + _cwd: PathBuf, env: HashMap, cancel_rx: CancellationToken, ) -> anyhow::Result { @@ -315,8 +587,8 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { let result = crate::sandboxing::execute_env( crate::sandboxing::ExecRequest { - command, - cwd, + command: self.command.clone(), + cwd: self.cwd.clone(), env: exec_env, network: self.network.clone(), expiration: ExecExpiration::Cancellation(cancel_rx), @@ -340,34 +612,126 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { timed_out: result.timed_out, }) } + + async fn prepare_escalated_exec( + &self, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, + env: HashMap, + execution: EscalationExecution, + ) -> anyhow::Result { + let command = join_program_and_argv(program, argv); + let Some(first_arg) = argv.first() else { + return Err(anyhow::anyhow!( + "intercepted exec request must contain argv[0]" + )); + }; + + let prepared = match execution { + EscalationExecution::Unsandboxed => PreparedExec { + command, + cwd: workdir.to_path_buf(), + env, + arg0: Some(first_arg.clone()), + }, + EscalationExecution::TurnDefault => self.prepare_sandboxed_exec( + command, + workdir, + env, + &self.sandbox_policy, + None, + self.macos_seatbelt_profile_extensions.as_ref(), + )?, + EscalationExecution::Permissions(EscalationPermissions::PermissionProfile( + permission_profile, + )) => self.prepare_sandboxed_exec( + command, + workdir, + env, + &self.sandbox_policy, + Some(permission_profile), + None, + )?, + EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => { + self.prepare_sandboxed_exec( + command, + workdir, + env, + &permissions.sandbox_policy, + None, + permissions.macos_seatbelt_profile_extensions.as_ref(), + )? + } + }; + + Ok(prepared) + } } -// TODO(mbolin): This should be passed down from codex-arg0 like codex_linux_sandbox_exe. -fn shell_execve_wrapper() -> anyhow::Result { - const EXECVE_WRAPPER: &str = "codex-execve-wrapper"; - - if let Some(path) = std::env::var_os("PATH") { - for dir in std::env::split_paths(&path) { - let candidate = dir.join(EXECVE_WRAPPER); - if candidate.is_file() { - return Ok(candidate); - } +impl CoreShellCommandExecutor { + fn prepare_sandboxed_exec( + &self, + command: Vec, + workdir: &AbsolutePathBuf, + env: HashMap, + sandbox_policy: &SandboxPolicy, + additional_permissions: Option, + #[cfg(target_os = "macos")] macos_seatbelt_profile_extensions: Option< + &MacOsSeatbeltProfileExtensions, + >, + #[cfg(not(target_os = "macos"))] _macos_seatbelt_profile_extensions: Option< + &MacOsSeatbeltProfileExtensions, + >, + ) -> anyhow::Result { + let (program, args) = command + .split_first() + .ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?; + let sandbox_manager = crate::sandboxing::SandboxManager::new(); + let sandbox = sandbox_manager.select_initial( + sandbox_policy, + SandboxablePreference::Auto, + self.windows_sandbox_level, + self.network.is_some(), + ); + let mut exec_request = + sandbox_manager.transform(crate::sandboxing::SandboxTransformRequest { + spec: crate::sandboxing::CommandSpec { + program: program.clone(), + args: args.to_vec(), + cwd: workdir.to_path_buf(), + env, + expiration: ExecExpiration::DefaultTimeout, + sandbox_permissions: if additional_permissions.is_some() { + SandboxPermissions::WithAdditionalPermissions + } else { + SandboxPermissions::UseDefault + }, + additional_permissions, + justification: self.justification.clone(), + }, + policy: sandbox_policy, + sandbox, + enforce_managed_network: self.network.is_some(), + network: self.network.as_ref(), + sandbox_policy_cwd: &self.sandbox_policy_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions, + codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), + use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap, + windows_sandbox_level: self.windows_sandbox_level, + })?; + if let Some(network) = exec_request.network.as_ref() { + network.apply_to_env(&mut exec_request.env); } - } - let exe = std::env::current_exe()?; - let sibling = exe - .parent() - .map(|parent| parent.join(EXECVE_WRAPPER)) - .ok_or_else(|| anyhow::anyhow!("failed to determine codex-execve-wrapper path"))?; - if sibling.is_file() { - return Ok(sibling); + Ok(PreparedExec { + command: exec_request.command, + cwd: exec_request.cwd, + env: exec_request.env, + arg0: exec_request.arg0, + }) } - - Err(anyhow::anyhow!( - "failed to locate {EXECVE_WRAPPER} in PATH or next to current executable ({})", - exe.display() - )) } #[derive(Debug, Eq, PartialEq)] @@ -377,19 +741,20 @@ struct ParsedShellCommand { } fn extract_shell_script(command: &[String]) -> Result { - match command { - [_, flag, script, ..] if flag == "-c" => Ok(ParsedShellCommand { - script: script.clone(), - login: false, - }), - [_, flag, script, ..] if flag == "-lc" => Ok(ParsedShellCommand { - script: script.clone(), - login: true, - }), - _ => Err(ToolError::Rejected( - "unexpected shell command format for zsh-fork execution".to_string(), - )), + // Commands reaching zsh-fork can be wrapped by environment/sandbox helpers, so + // we search for the first `-c`/`-lc` triple anywhere in the argv rather + // than assuming it is the first positional form. + if let Some((script, login)) = command.windows(3).find_map(|parts| match parts { + [_, flag, script] if flag == "-c" => Some((script.to_owned(), false)), + [_, flag, script] if flag == "-lc" => Some((script.to_owned(), true)), + _ => None, + }) { + return Ok(ParsedShellCommand { script, login }); } + + Err(ToolError::Rejected( + "unexpected shell command format for zsh-fork execution".to_string(), + )) } fn map_exec_result( @@ -421,51 +786,18 @@ fn map_exec_result( Ok(output) } -#[cfg(test)] -mod tests { - use super::ParsedShellCommand; - use super::extract_shell_script; - use super::map_exec_result; - use crate::exec::SandboxType; - use codex_shell_escalation::ExecResult; - use pretty_assertions::assert_eq; - use std::time::Duration; - - #[test] - fn extract_shell_script_preserves_login_flag() { - assert_eq!( - extract_shell_script(&["/bin/zsh".into(), "-lc".into(), "echo hi".into()]).unwrap(), - ParsedShellCommand { - script: "echo hi".to_string(), - login: true, - } - ); - assert_eq!( - extract_shell_script(&["/bin/zsh".into(), "-c".into(), "echo hi".into()]).unwrap(), - ParsedShellCommand { - script: "echo hi".to_string(), - login: false, - } - ); - } - - #[test] - fn map_exec_result_preserves_stdout_and_stderr() { - let out = map_exec_result( - SandboxType::None, - ExecResult { - exit_code: 0, - stdout: "out".to_string(), - stderr: "err".to_string(), - output: "outerr".to_string(), - duration: Duration::from_millis(1), - timed_out: false, - }, - ) - .unwrap(); - - assert_eq!(out.stdout.text, "out"); - assert_eq!(out.stderr.text, "err"); - assert_eq!(out.aggregated_output.text, "outerr"); - } +/// Convert an intercepted exec `(program, argv)` into a command vector suitable +/// for display and policy parsing. +/// +/// The intercepted `argv` includes `argv[0]`, but once we have normalized the +/// executable path in `program`, we should replace the original `argv[0]` +/// rather than duplicating it as an apparent user argument. +fn join_program_and_argv(program: &AbsolutePathBuf, argv: &[String]) -> Vec { + std::iter::once(program.to_string_lossy().to_string()) + .chain(argv.iter().skip(1).cloned()) + .collect::>() } + +#[cfg(test)] +#[path = "unix_escalation_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs new file mode 100644 index 0000000000..519608a43b --- /dev/null +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -0,0 +1,321 @@ +use super::CoreShellActionProvider; +#[cfg(target_os = "macos")] +use super::CoreShellCommandExecutor; +use super::ParsedShellCommand; +use super::extract_shell_script; +use super::join_program_and_argv; +use super::map_exec_result; +#[cfg(target_os = "macos")] +use crate::config::Constrained; +#[cfg(target_os = "macos")] +use crate::config::Permissions; +#[cfg(target_os = "macos")] +use crate::config::types::ShellEnvironmentPolicy; +use crate::exec::SandboxType; +#[cfg(target_os = "macos")] +use crate::protocol::AskForApproval; +use crate::protocol::ReadOnlyAccess; +use crate::protocol::SandboxPolicy; +#[cfg(target_os = "macos")] +use crate::sandboxing::SandboxPermissions; +#[cfg(target_os = "macos")] +use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; +#[cfg(target_os = "macos")] +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::MacOsPreferencesPermission; +use codex_protocol::models::MacOsSeatbeltProfileExtensions; +use codex_protocol::models::PermissionProfile; +use codex_shell_escalation::EscalationExecution; +use codex_shell_escalation::EscalationPermissions; +use codex_shell_escalation::ExecResult; +use codex_shell_escalation::Permissions as EscalatedPermissions; +#[cfg(target_os = "macos")] +use codex_shell_escalation::ShellCommandExecutor; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +#[cfg(target_os = "macos")] +use std::collections::HashMap; +use std::time::Duration; + +#[test] +fn extract_shell_script_preserves_login_flag() { + assert_eq!( + extract_shell_script(&["/bin/zsh".into(), "-lc".into(), "echo hi".into()]).unwrap(), + ParsedShellCommand { + script: "echo hi".to_string(), + login: true, + } + ); + assert_eq!( + extract_shell_script(&["/bin/zsh".into(), "-c".into(), "echo hi".into()]).unwrap(), + ParsedShellCommand { + script: "echo hi".to_string(), + login: false, + } + ); +} + +#[test] +fn extract_shell_script_supports_wrapped_command_prefixes() { + assert_eq!( + extract_shell_script(&[ + "/usr/bin/env".into(), + "CODEX_EXECVE_WRAPPER=1".into(), + "/bin/zsh".into(), + "-lc".into(), + "echo hello".into() + ]) + .unwrap(), + ParsedShellCommand { + script: "echo hello".to_string(), + login: true, + } + ); + + assert_eq!( + extract_shell_script(&[ + "sandbox-exec".into(), + "-p".into(), + "sandbox_policy".into(), + "/bin/zsh".into(), + "-c".into(), + "pwd".into(), + ]) + .unwrap(), + ParsedShellCommand { + script: "pwd".to_string(), + login: false, + } + ); +} + +#[test] +fn extract_shell_script_rejects_unsupported_shell_invocation() { + let err = extract_shell_script(&[ + "sandbox-exec".into(), + "-fc".into(), + "echo not supported".into(), + ]) + .unwrap_err(); + assert!(matches!(err, super::ToolError::Rejected(_))); + assert_eq!( + match err { + super::ToolError::Rejected(reason) => reason, + _ => "".to_string(), + }, + "unexpected shell command format for zsh-fork execution" + ); +} + +#[test] +fn join_program_and_argv_replaces_original_argv_zero() { + assert_eq!( + join_program_and_argv( + &AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(), + &["./tool".into(), "--flag".into(), "value".into()], + ), + vec!["/tmp/tool", "--flag", "value"] + ); + assert_eq!( + join_program_and_argv( + &AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(), + &["./tool".into()] + ), + vec!["/tmp/tool"] + ); +} + +#[test] +fn map_exec_result_preserves_stdout_and_stderr() { + let out = map_exec_result( + SandboxType::None, + ExecResult { + exit_code: 0, + stdout: "out".to_string(), + stderr: "err".to_string(), + output: "outerr".to_string(), + duration: Duration::from_millis(1), + timed_out: false, + }, + ) + .unwrap(); + + assert_eq!(out.stdout.text, "out"); + assert_eq!(out.stderr.text, "err"); + assert_eq!(out.aggregated_output.text, "outerr"); +} + +#[test] +fn shell_request_escalation_execution_is_explicit() { + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![ + AbsolutePathBuf::from_absolute_path("/tmp/output").unwrap(), + ]), + }), + ..Default::default() + }; + let sandbox_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap()], + read_only_access: ReadOnlyAccess::FullAccess, + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::ReadWrite, + ..Default::default() + }; + + assert_eq!( + CoreShellActionProvider::shell_request_escalation_execution( + crate::sandboxing::SandboxPermissions::UseDefault, + &sandbox_policy, + None, + Some(&macos_seatbelt_profile_extensions), + ), + EscalationExecution::TurnDefault, + ); + assert_eq!( + CoreShellActionProvider::shell_request_escalation_execution( + crate::sandboxing::SandboxPermissions::RequireEscalated, + &sandbox_policy, + None, + Some(&macos_seatbelt_profile_extensions), + ), + EscalationExecution::Unsandboxed, + ); + assert_eq!( + CoreShellActionProvider::shell_request_escalation_execution( + crate::sandboxing::SandboxPermissions::WithAdditionalPermissions, + &sandbox_policy, + Some(&requested_permissions), + Some(&macos_seatbelt_profile_extensions), + ), + EscalationExecution::Permissions(EscalationPermissions::Permissions( + EscalatedPermissions { + sandbox_policy, + macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions), + }, + )), + ); +} + +#[cfg(target_os = "macos")] +#[tokio::test] +async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap(); + let executor = CoreShellCommandExecutor { + command: vec!["echo".to_string(), "ok".to_string()], + cwd: cwd.to_path_buf(), + env: HashMap::new(), + network: None, + sandbox: SandboxType::None, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + windows_sandbox_level: WindowsSandboxLevel::Disabled, + sandbox_permissions: SandboxPermissions::UseDefault, + justification: None, + arg0: None, + sandbox_policy_cwd: cwd.to_path_buf(), + macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::ReadWrite, + ..Default::default() + }), + codex_linux_sandbox_exe: None, + use_linux_sandbox_bwrap: false, + }; + + let prepared = executor + .prepare_escalated_exec( + &AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(), + &["echo".to_string(), "ok".to_string()], + &cwd, + HashMap::new(), + EscalationExecution::TurnDefault, + ) + .await + .unwrap(); + + assert_eq!( + prepared.command.first().map(String::as_str), + Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE) + ); + assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p")); + assert!( + prepared + .command + .get(2) + .is_some_and(|policy| policy.contains("(allow user-preference-write)")), + "expected seatbelt policy to include macOS extension profile: {:?}", + prepared.command + ); +} + +#[cfg(target_os = "macos")] +#[tokio::test] +async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap(); + let executor = CoreShellCommandExecutor { + command: vec!["echo".to_string(), "ok".to_string()], + cwd: cwd.to_path_buf(), + env: HashMap::new(), + network: None, + sandbox: SandboxType::None, + sandbox_policy: SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + sandbox_permissions: SandboxPermissions::UseDefault, + justification: None, + arg0: None, + sandbox_policy_cwd: cwd.to_path_buf(), + macos_seatbelt_profile_extensions: None, + codex_linux_sandbox_exe: None, + use_linux_sandbox_bwrap: false, + }; + + let permissions = Permissions { + approval_policy: Constrained::allow_any(AskForApproval::Never), + sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + network: None, + allow_login_shell: true, + shell_environment_policy: ShellEnvironmentPolicy::default(), + windows_sandbox_mode: None, + macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions { + macos_preferences: MacOsPreferencesPermission::ReadWrite, + ..Default::default() + }), + }; + + let prepared = executor + .prepare_escalated_exec( + &AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(), + &["echo".to_string(), "ok".to_string()], + &cwd, + HashMap::new(), + EscalationExecution::Permissions(EscalationPermissions::Permissions( + EscalatedPermissions { + sandbox_policy: permissions.sandbox_policy.get().clone(), + macos_seatbelt_profile_extensions: permissions + .macos_seatbelt_profile_extensions + .clone(), + }, + )), + ) + .await + .unwrap(); + + assert_eq!( + prepared.command.first().map(String::as_str), + Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE) + ); + assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p")); + assert!( + prepared + .command + .get(2) + .is_some_and(|policy| policy.contains("(allow user-preference-write)")), + "expected seatbelt policy to include macOS extension profile: {:?}", + prepared.command + ); +} diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 893dae27e5..f4d823bf0e 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -32,7 +32,7 @@ use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::NetworkProxy; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; use futures::future::BoxFuture; use std::collections::HashMap; @@ -47,7 +47,7 @@ pub struct UnifiedExecRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub justification: Option, pub exec_approval_requirement: ExecApprovalRequirement, } @@ -58,7 +58,7 @@ pub struct UnifiedExecApprovalKey { pub cwd: PathBuf, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, } pub struct UnifiedExecRuntime<'a> { @@ -111,6 +111,7 @@ impl Approvable for UnifiedExecRuntime<'_> { .or_else(|| req.justification.clone()); Box::pin(async move { with_cached_approval(&session.services, "unified_exec", keys, || async move { + let available_decisions = None; session .request_command_approval( turn, @@ -124,6 +125,7 @@ impl Approvable for UnifiedExecRuntime<'_> { .proposed_execpolicy_amendment() .cloned(), req.additional_permissions.clone(), + available_decisions, ) .await }) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 25ea101592..28d87b5bf3 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -340,6 +340,8 @@ impl<'a> SandboxAttempt<'a> { enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, + #[cfg(target_os = "macos")] + macos_seatbelt_profile_extensions: None, codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, use_linux_sandbox_bwrap: self.use_linux_sandbox_bwrap, windows_sandbox_level: self.windows_sandbox_level, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 3c9ab4bf3a..76730d0890 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -6,6 +6,7 @@ use crate::config::AgentRoleConfig; use crate::features::Feature; use crate::features::Features; use crate::mcp_connection_manager::ToolInfo; +use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::SEARCH_TOOL_BM25_DEFAULT_LIMIT; use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME; @@ -54,7 +55,7 @@ pub(crate) struct ToolsConfig { pub js_repl_enabled: bool, pub js_repl_tools_only: bool, pub collab_tools: bool, - pub collaboration_modes_tools: bool, + pub default_mode_request_user_input: bool, pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, @@ -80,7 +81,8 @@ impl ToolsConfig { let include_js_repl_tools_only = include_js_repl && features.enabled(Feature::JsReplToolsOnly); let include_collab_tools = features.enabled(Feature::Collab); - let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes); + let include_default_mode_request_user_input = + features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = features.enabled(Feature::Apps); let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite); let request_permission_enabled = features.enabled(Feature::RequestPermissions); @@ -137,7 +139,7 @@ impl ToolsConfig { js_repl_enabled: include_js_repl, js_repl_tools_only: include_js_repl_tools_only, collab_tools: include_collab_tools, - collaboration_modes_tools: include_collaboration_modes_tools, + default_mode_request_user_input: include_default_mode_request_user_input, experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, @@ -256,29 +258,36 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap ToolSpec { )), }, ), + ( + "fork_context".to_string(), + JsonSchema::Boolean { + description: Some( + "When true, fork the current thread history into the new agent before sending the initial prompt. This must be used when you want the new agent to have exactly the same context as you." + .to_string(), + ), + }, + ), ]); ToolSpec::Function(ResponsesApiTool { @@ -865,7 +883,9 @@ fn create_wait_tool() -> ToolSpec { }) } -fn create_request_user_input_tool() -> ToolSpec { +fn create_request_user_input_tool( + collaboration_modes_config: CollaborationModesConfig, +) -> ToolSpec { let mut option_props = BTreeMap::new(); option_props.insert( "label".to_string(), @@ -936,7 +956,9 @@ fn create_request_user_input_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "request_user_input".to_string(), - description: request_user_input_tool_description(), + description: request_user_input_tool_description( + collaboration_modes_config.default_mode_request_user_input, + ), strict: false, parameters: JsonSchema::Object { properties, @@ -1657,7 +1679,9 @@ pub(crate) fn build_specs( let mcp_handler = Arc::new(McpHandler); let mcp_resource_handler = Arc::new(McpResourceHandler); let shell_command_handler = Arc::new(ShellCommandHandler::from(config.shell_command_backend)); - let request_user_input_handler = Arc::new(RequestUserInputHandler); + let request_user_input_handler = Arc::new(RequestUserInputHandler { + default_mode_request_user_input: config.default_mode_request_user_input, + }); let search_tool_handler = Arc::new(SearchToolBm25Handler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); @@ -1720,10 +1744,10 @@ pub(crate) fn build_specs( builder.register_handler("js_repl_reset", js_repl_reset_handler); } - if config.collaboration_modes_tools { - builder.push_spec(create_request_user_input_tool()); - builder.register_handler("request_user_input", request_user_input_handler); - } + builder.push_spec(create_request_user_input_tool(CollaborationModesConfig { + default_mode_request_user_input: config.default_mode_request_user_input, + })); + builder.register_handler("request_user_input", request_user_input_handler); if config.search_tool && let Some(app_tools) = app_tools @@ -2017,7 +2041,6 @@ mod tests { let model_info = model_info_from_models_json("gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2048,7 +2071,7 @@ mod tests { create_exec_command_tool(true, false), create_write_stdin_tool(), PLAN_TOOL.clone(), - create_request_user_input_tool(), + create_request_user_input_tool(CollaborationModesConfig::default()), create_apply_patch_freeform_tool(), ToolSpec::WebSearch { external_web_access: Some(true), @@ -2080,8 +2103,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); - features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2108,7 +2129,6 @@ mod tests { ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::Collab); - features.enable(Feature::CollaborationModes); features.enable(Feature::Sqlite); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -2134,12 +2154,11 @@ mod tests { } #[test] - fn request_user_input_requires_collaboration_modes_feature() { + fn request_user_input_description_reflects_default_mode_feature_flag() { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); - features.disable(Feature::CollaborationModes); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2147,12 +2166,13 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert!( - !tools.iter().any(|t| t.spec.name() == "request_user_input"), - "request_user_input should be disabled when collaboration_modes feature is off" + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(CollaborationModesConfig::default()) ); - features.enable(Feature::CollaborationModes); + features.enable(Feature::DefaultModeRequestUserInput); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2160,7 +2180,13 @@ mod tests { session_source: SessionSource::Cli, }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); - assert_contains_tool_names(&tools, &["request_user_input"]); + let request_user_input_tool = find_tool(&tools, "request_user_input"); + assert_eq!( + request_user_input_tool.spec, + create_request_user_input_tool(CollaborationModesConfig { + default_mode_request_user_input: true, + }) + ); } #[test] @@ -2328,8 +2354,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2352,8 +2377,7 @@ mod tests { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, @@ -2374,8 +2398,7 @@ mod tests { #[test] fn test_build_specs_gpt5_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5-codex", &features, @@ -2393,8 +2416,7 @@ mod tests { #[test] fn test_build_specs_gpt51_codex_default() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex", &features, @@ -2414,7 +2436,6 @@ mod tests { fn test_build_specs_gpt5_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5-codex", &features, @@ -2435,7 +2456,6 @@ mod tests { fn test_build_specs_gpt51_codex_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex", &features, @@ -2454,8 +2474,7 @@ mod tests { #[test] fn test_gpt_5_1_codex_max_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-max", &features, @@ -2473,8 +2492,7 @@ mod tests { #[test] fn test_codex_5_1_mini_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1-codex-mini", &features, @@ -2492,8 +2510,7 @@ mod tests { #[test] fn test_gpt_5_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5", &features, @@ -2510,8 +2527,7 @@ mod tests { #[test] fn test_gpt_5_1_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); + let features = Features::with_defaults(); assert_default_model_tools( "gpt-5.1", &features, @@ -2531,7 +2547,6 @@ mod tests { fn test_gpt_5_1_codex_max_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); - features.enable(Feature::CollaborationModes); assert_model_tools( "gpt-5.1-codex-max", &features, diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 1481d30136..1d949dc2b9 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -29,7 +29,7 @@ use std::sync::Weak; use std::time::Duration; use codex_network_proxy::NetworkProxy; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use rand::Rng; use rand::rng; use tokio::sync::Mutex; @@ -90,7 +90,7 @@ pub(crate) struct ExecCommandRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub justification: Option, pub prefix_rule: Option>, } diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index 80128df006..e7921c69f3 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -1,21 +1,12 @@ use std::time::Duration; -use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use crate::codex::TurnContext; +use crate::contextual_user_message::USER_SHELL_COMMAND_FRAGMENT; use crate::exec::ExecToolCallOutput; use crate::tools::format_exec_output_str; -pub const USER_SHELL_COMMAND_OPEN: &str = ""; -pub const USER_SHELL_COMMAND_CLOSE: &str = ""; - -pub fn is_user_shell_command_text(text: &str) -> bool { - let trimmed = text.trim_start(); - let lowered = trimmed.to_ascii_lowercase(); - lowered.starts_with(USER_SHELL_COMMAND_OPEN) -} - fn format_duration_line(duration: Duration) -> String { let duration_seconds = duration.as_secs_f64(); format!("Duration: {duration_seconds:.4} seconds") @@ -48,7 +39,7 @@ pub fn format_user_shell_command_record( turn_context: &TurnContext, ) -> String { let body = format_user_shell_command_body(command, exec_output, turn_context); - format!("{USER_SHELL_COMMAND_OPEN}\n{body}\n{USER_SHELL_COMMAND_CLOSE}") + USER_SHELL_COMMAND_FRAGMENT.wrap(body) } pub fn user_shell_command_record_item( @@ -56,15 +47,11 @@ pub fn user_shell_command_record_item( exec_output: &ExecToolCallOutput, turn_context: &TurnContext, ) -> ResponseItem { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: format_user_shell_command_record(command, exec_output, turn_context), - }], - end_turn: None, - phase: None, - } + USER_SHELL_COMMAND_FRAGMENT.into_message(format_user_shell_command_record( + command, + exec_output, + turn_context, + )) } #[cfg(test)] @@ -72,14 +59,16 @@ mod tests { use super::*; use crate::codex::make_session_and_context; use crate::exec::StreamOutput; + use codex_protocol::models::ContentItem; use pretty_assertions::assert_eq; #[test] fn detects_user_shell_command_text_variants() { - assert!(is_user_shell_command_text( - "\necho hi\n" - )); - assert!(!is_user_shell_command_text("echo hi")); + assert!( + USER_SHELL_COMMAND_FRAGMENT + .matches_text("\necho hi\n") + ); + assert!(!USER_SHELL_COMMAND_FRAGMENT.matches_text("echo hi")); } #[tokio::test] diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/core/templates/collaboration_mode/default.md index 4efd963ba0..ff00857c6d 100644 --- a/codex-rs/core/templates/collaboration_mode/default.md +++ b/codex-rs/core/templates/collaboration_mode/default.md @@ -8,4 +8,4 @@ Your active mode changes only when new developer instructions with a different ` {{REQUEST_USER_INPUT_AVAILABILITY}} -If a decision is necessary and cannot be discovered from local context, ask the user directly. However, in Default mode you should strongly prefer executing the user's request rather than stopping to ask questions. +{{ASKING_QUESTIONS_GUIDANCE}} diff --git a/codex-rs/core/templates/memories/consolidation.md b/codex-rs/core/templates/memories/consolidation.md index b880bdfd65..085895a69f 100644 --- a/codex-rs/core/templates/memories/consolidation.md +++ b/codex-rs/core/templates/memories/consolidation.md @@ -106,7 +106,7 @@ Under `{{ memory_root }}/`: context. - source of rollout-level metadata needed for MEMORY.md `### rollout_summary_files` annotations; - you should be able to find `cwd` and `updated_at` there. + you should be able to find `cwd`, `rollout_path`, and `updated_at` there. - `MEMORY.md` - merged memories; produce a lightly clustered version if applicable - `rollout_summaries/*.md` @@ -121,6 +121,27 @@ Mode selection: - INCREMENTAL UPDATE: existing artifacts already exist and `raw_memories.md` mostly contains new additions. +Incremental thread diff snapshot (computed before the current artifact sync rewrites local files): + +**Diff since last consolidation:** +{{ phase2_input_selection }} + +Incremental update and forgetting mechanism: +- Use the diff provided +- Do not open raw sessions / original rollout transcripts. +- For each added thread id, search it in `raw_memories.md`, read that raw-memory section, and + read the corresponding `rollout_summaries/*.md` file only when needed for stronger evidence, + task placement, or conflict resolution. +- For each removed thread id, search it in `MEMORY.md` and delete only the memory supported by + that thread. Use `thread_id=` in `### rollout_summary_files` when available; if not, + fall back to rollout summary filenames plus the corresponding `rollout_summaries/*.md` files. +- If a `MEMORY.md` block contains both removed and undeleted threads, do not delete the whole + block. Remove only the removed thread's references and thread-local learnings, preserve shared + or still-supported content, and split or rewrite the block only if needed to keep the undeleted + threads intact. +- After `MEMORY.md` cleanup is done, revisit `memory_summary.md` and remove or rewrite stale + summary/index content that was only supported by removed thread ids. + Outputs: Under `{{ memory_root }}/`: A) `MEMORY.md` @@ -171,7 +192,7 @@ Required task-oriented body shape (strict): ## Task 1: ### rollout_summary_files -- (cwd=, updated_at=, thread_id=, ) +- (cwd=, rollout_path=, updated_at=, thread_id=, ) ### keywords @@ -236,7 +257,8 @@ Schema rules (strict): - Every `## Task ` section must include `### rollout_summary_files`, `### keywords`, and `### learnings`. - `### rollout_summary_files` must be task-local (not a block-wide catch-all list). - - Each rollout annotation must include `cwd=` and `updated_at=`. + - Each rollout annotation must include `cwd=`, `rollout_path=`, and + `updated_at=`. If missing from a rollout summary, recover them from `raw_memories.md`. - Major learnings should be traceable to rollout summaries listed in the same task section. - Order rollout references by freshness and practical usefulness. @@ -497,27 +519,42 @@ WORKFLOW conflicting task families until MEMORY blocks are richer and more useful than raw memories 3) INCREMENTAL UPDATE behavior: - - Treat `raw_memories.md` as the primary source of NEW signal. - - Read existing memory files first for continuity. + - Read existing `MEMORY.md` and `memory_summary.md` first for continuity and to locate + existing references that may need surgical cleanup. + - Use the injected thread-diff snapshot as the first routing pass: + - added thread ids = ingestion queue + - removed thread ids = forgetting / stale-cleanup queue - Build an index of rollout references already present in existing `MEMORY.md` before scanning raw memories so you can route net-new evidence into the right blocks. - - Compute net-new candidates from the raw-memory inventory (threads / rollout summaries / - updated evidence not already represented in `MEMORY.md`). + - Work in this order: + 1. For newly added thread ids, search them in `raw_memories.md`, read those sections, and + open the corresponding `rollout_summaries/*.md` files when necessary. + 2. Route the new signal into existing `MEMORY.md` blocks or create new ones when needed. + 3. For removed thread ids, search `MEMORY.md` and surgically delete or rewrite only the + unsupported thread-local memory. + 4. If a block mixes removed and undeleted threads, preserve the undeleted-thread content; + split or rewrite the block if that is the cleanest way to delete only the removed part. + 5. After `MEMORY.md` is correct, revisit `memory_summary.md` and remove or rewrite stale + summary/index content that no longer has undeleted support. - Integrate new signal into existing artifacts by: - - scanning new raw memories in recency order and identifying which existing blocks they should update + - scanning the newly added raw-memory entries in recency order and identifying which existing blocks they should update - updating existing knowledge with better/newer evidence - updating stale or contradicting guidance + - pruning or downgrading memory whose only provenance comes from removed thread ids - expanding terse old blocks when new summaries/raw memories make the task family clearer - doing light clustering and merging if needed - refreshing `MEMORY.md` top-of-file ordering so recent high-utility task families stay easy to find - rebuilding the `memory_summary.md` recent active window (last 3 memory days) from current `updated_at` coverage - updating existing skills or adding new skills only when there is clear new reusable procedure - - update `memory_summary.md` last to reflect the final state of the memory folder + - updating `memory_summary.md` last to reflect the final state of the memory folder - Minimize churn in incremental mode: if an existing `MEMORY.md` block or `## What's in Memory` topic still reflects the current evidence and points to the same task family / retrieval target, keep its wording, label, and relative order mostly stable. Rewrite/reorder/rename/ split/merge only when fixing a real problem (staleness, ambiguity, schema drift, wrong boundaries) or when meaningful new evidence materially improves retrieval clarity/searchability. + - Spend most of your deep-dive budget on newly added thread ids and on mixed blocks touched by + removed thread ids. Do not re-read unchanged older threads unless you need them for + conflict resolution, clustering, or provenance repair. 4) Evidence deep-dive rule (both modes): - `raw_memories.md` is the routing layer, not always the final authority for detail. @@ -528,6 +565,9 @@ WORKFLOW - When a task family is important, ambiguous, or duplicated across multiple rollouts, open the relevant `rollout_summaries/*.md` files and extract richer procedural detail, validation signals, and user feedback before finalizing `MEMORY.md`. + - When deleting stale memory from a mixed block, use the relevant rollout summaries to decide + which details are uniquely supported by removed threads versus still supported by undeleted + threads. - Use `updated_at` and validation strength together to resolve stale/conflicting notes. 5) For both modes, update `MEMORY.md` after skill updates: @@ -541,6 +581,8 @@ WORKFLOW 7) Final pass: - remove duplication in memory_summary, skills/, and MEMORY.md - remove stale or low-signal blocks that are less likely to be useful in the future + - remove or rewrite blocks/task sections whose supporting rollout references point only to + removed thread ids or missing rollout summary files - run a global rollout-reference audit on final `MEMORY.md` and fix accidental duplicate entries / redundant repetition, while preserving intentional multi-task or multi-block reuse when it adds distinct task-local value @@ -559,16 +601,3 @@ WORKFLOW You should dive deep and make sure you didn't miss any important information that might be useful for future agents; do not be superficial. - -============================================================ -SEARCH / REVIEW COMMANDS (RG-FIRST) -============================================================ - -Use `rg` for fast retrieval while consolidating: - -- Search durable notes: - `rg -n -i "" "{{ memory_root }}/MEMORY.md"` -- Search across memory tree: - `rg -n -i "" "{{ memory_root }}" | head -n 100` -- Locate rollout summary files: - `rg --files "{{ memory_root }}/rollout_summaries" | head -n 400` diff --git a/codex-rs/core/templates/memories/read_path.md b/codex-rs/core/templates/memories/read_path.md index 7fd38ca50e..ba0e21044e 100644 --- a/codex-rs/core/templates/memories/read_path.md +++ b/codex-rs/core/templates/memories/read_path.md @@ -25,7 +25,10 @@ Memory layout (general -> specific): - scripts/ (optional helper scripts) - examples/ (optional example outputs) - templates/ (optional templates) -- {{ base_path }}/rollout_summaries/ (per-rollout recaps + evidence snippets) + - {{ base_path }}/rollout_summaries/ (per-rollout recaps + evidence snippets) + - The paths of these entries can be found in {{ base_path }}/MEMORY.md or {{ base_path }}/rollout_summaries/ as `rollout_path` + - These files are append-only `jsonl`: `session_meta.payload.id` identifies the session, `turn_context` marks turn boundaries, `event_msg` is the lightweight status stream, and `response_item` contains actual messages, tool calls, and tool outputs. + - For efficient lookup, prefer matching the filename suffix or `session_meta.payload.id`; avoid broad full-content scans unless needed. Quick memory pass (when applicable): @@ -34,7 +37,8 @@ Quick memory pass (when applicable): 3. Only if MEMORY.md directly points to rollout summaries/skills, open the 1-2 most relevant files under {{ base_path }}/rollout_summaries/ or {{ base_path }}/skills/. -4. If there are no relevant hits, stop memory lookup and continue normally. +4. If above are not clear and you need exact commands, error text, or precise evidence, search over `rollout_path` for more evidence. +5. If there are no relevant hits, stop memory lookup and continue normally. Quick-pass budget: @@ -52,6 +56,47 @@ When to update memory: - When user explicitly asks to remember/update memory, revise memory_summary.md and/or MEMORY.md. +Memory citation requirements: + +- If ANY relevant memory files were used: append exactly one +`` block as the VERY LAST content of the final reply. + Normal responses should include the answer first, then append the +`` block at the end. +- Use this exact structure for programmatic parsing: +``` + + +MEMORY.md:234-236|note=[responsesapi citation extraction code pointer] +rollout_summaries/2026-02-17T21-23-02-LN3m-weekly_memory_report_pivot_from_git_history.md:10-12|note=[weekly report format] + + +019c6e27-e55b-73d1-87d8-4e01f1f75043 +019c7714-3b77-74d1-9866-e1f484aae2ab + + +``` +- `citation_entries` is for rendering: + - one citation entry per line + - format: `:-|note=[]` + - use file paths relative to the memory base path (for example, `MEMORY.md`, + `rollout_summaries/...`, `skills/...`) + - only cite files actually used under the memory base path (do not cite + workspace files as memory citations) + - if you used `MEMORY.md` and then a rollout summary/skill file, cite both + - list entries in order of importance (most important first) + - `note` should be short, single-line, and use simple characters only (avoid + unusual symbols, no newlines) +- `rollout_ids` is for us to track what previous rollouts you find useful: + - include one rollout id per line + - rollout ids should look like UUIDs (for example, + `019c6e27-e55b-73d1-87d8-4e01f1f75043`) + - include unique ids only; do not repeat ids + - an empty `` section is allowed if no rollout ids are available + - you can find rollout ids in rollout summary files and MEMORY.md + - do not include file paths or notes in this section +- Never include memory citations inside pull-request messages. +- Never cite blank lines; double-check ranges. + ========= MEMORY_SUMMARY BEGINS ========= {{ memory_summary }} ========= MEMORY_SUMMARY ENDS ========= diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs index f9362ee30c..addb1bc31e 100644 --- a/codex-rs/core/tests/common/context_snapshot.rs +++ b/codex-rs/core/tests/common/context_snapshot.rs @@ -62,7 +62,7 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot match item_type { "message" => { let role = item.get("role").and_then(Value::as_str).unwrap_or("unknown"); - let text = item + let rendered_parts = item .get("content") .and_then(Value::as_array) .map(|content| { @@ -93,11 +93,27 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot } }) .collect::>() - .join(" | ") }) - .filter(|text| !text.is_empty()) - .unwrap_or_else(|| "".to_string()); - format!("{idx:02}:message/{role}:{text}") + .unwrap_or_default(); + let role = if rendered_parts.len() > 1 { + format!("{role}[{}]", rendered_parts.len()) + } else { + role.to_string() + }; + if rendered_parts.is_empty() { + return format!("{idx:02}:message/{role}:"); + } + if rendered_parts.len() == 1 { + return format!("{idx:02}:message/{role}:{}", rendered_parts[0]); + } + + let parts = rendered_parts + .iter() + .enumerate() + .map(|(part_idx, part)| format!(" [{:02}] {part}", part_idx + 1)) + .collect::>() + .join("\n"); + format!("{idx:02}:message/{role}:\n{parts}") } "function_call" => { let name = item.get("name").and_then(Value::as_str).unwrap_or("unknown"); @@ -222,15 +238,34 @@ fn canonicalize_snapshot_text(text: &str) -> String { return "".to_string(); } if text.starts_with("") { + let subagent_count = text + .split_once("") + .and_then(|(_, rest)| rest.split_once("")) + .map(|(subagents, _)| { + subagents + .lines() + .filter(|line| line.trim_start().starts_with("- ")) + .count() + }) + .unwrap_or(0); + let subagents_suffix = if subagent_count > 0 { + format!(":subagents={subagent_count}") + } else { + String::new() + }; if let (Some(cwd_start), Some(cwd_end)) = (text.find(""), text.find("")) { let cwd = &text[cwd_start + "".len()..cwd_end]; return if cwd.ends_with("PRETURN_CONTEXT_DIFF_CWD") { - "".to_string() + format!("") } else { - ">".to_string() + format!("{subagents_suffix}>") }; } - return "".to_string(); + return if subagent_count > 0 { + format!("") + } else { + "".to_string() + }; } if text.starts_with("You are performing a CONTEXT CHECKPOINT COMPACTION.") { return "".to_string(); @@ -258,7 +293,7 @@ mod tests { "role": "user", "content": [{ "type": "input_text", - "text": "# AGENTS.md instructions for /tmp/example" + "text": "# AGENTS.md instructions for /tmp/example\n\n\nbody\n" }] })]; @@ -269,7 +304,7 @@ mod tests { assert_eq!( rendered, - "00:message/user:# AGENTS.md instructions for /tmp/example" + r"00:message/user:# AGENTS.md instructions for /tmp/example\n\n\nbody\n" ); } @@ -280,7 +315,7 @@ mod tests { "role": "user", "content": [{ "type": "input_text", - "text": "# AGENTS.md instructions for /tmp/example" + "text": "# AGENTS.md instructions for /tmp/example\n\n\nbody\n" }] })]; @@ -292,6 +327,28 @@ mod tests { assert_eq!(rendered, "00:message/user:"); } + #[test] + fn redacted_text_mode_normalizes_environment_context_with_subagents() { + let items = vec![json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": "\n /tmp/example\n bash\n \n - agent-1: atlas\n - agent-2\n \n" + }] + })]; + + let rendered = format_response_items_snapshot( + &items, + &ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText), + ); + + assert_eq!( + rendered, + "00:message/user::subagents=2>" + ); + } + #[test] fn image_only_message_is_rendered_as_non_text_span() { let items = vec![json!({ @@ -333,7 +390,7 @@ mod tests { assert_eq!( rendered, - "00:message/user: | | " + "00:message/user[3]:\n [01] \n [02] \n [03] " ); } } diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index 4c7f26ddba..bb6de200a9 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -12,6 +12,7 @@ use futures::SinkExt; use futures::StreamExt; use serde_json::Value; use tokio::net::TcpListener; +use tokio::sync::Notify; use tokio::sync::oneshot; use tokio_tungstenite::accept_hdr_async_with_config; use tokio_tungstenite::tungstenite::Message; @@ -141,6 +142,32 @@ impl ResponsesRequest { .collect() } + /// Returns `input_text` spans grouped by `message` input for the provided role. + pub fn message_input_text_groups(&self, role: &str) -> Vec> { + self.inputs_of_type("message") + .into_iter() + .filter(|item| item.get("role").and_then(Value::as_str) == Some(role)) + .filter_map(|item| item.get("content").and_then(Value::as_array).cloned()) + .map(|content| { + content + .into_iter() + .filter(|span| span.get("type").and_then(Value::as_str) == Some("input_text")) + .filter_map(|span| span.get("text").and_then(Value::as_str).map(str::to_owned)) + .collect() + }) + .collect() + } + + pub fn has_message_with_input_texts( + &self, + role: &str, + predicate: impl Fn(&[String]) -> bool, + ) -> bool { + self.message_input_text_groups(role) + .iter() + .any(|texts| predicate(texts)) + } + /// Returns all `input_image` `image_url` spans from `message` inputs for the provided role. pub fn message_input_image_urls(&self, role: &str) -> Vec { self.inputs_of_type("message") @@ -237,7 +264,7 @@ impl ResponsesRequest { .cloned() .unwrap_or(Value::Null); match output { - Value::String(text) => Some((Some(text), None)), + Value::String(_) | Value::Array(_) => Some((output_value_to_text(&output), None)), Value::Object(obj) => Some(( obj.get("content") .and_then(Value::as_str) @@ -269,6 +296,87 @@ impl ResponsesRequest { } } +pub(crate) fn output_value_to_text(value: &Value) -> Option { + match value { + Value::String(text) => Some(text.clone()), + Value::Array(items) => match items.as_slice() { + [item] if item.get("type").and_then(Value::as_str) == Some("input_text") => { + item.get("text").and_then(Value::as_str).map(str::to_string) + } + [_] | [] | [_, _, ..] => None, + }, + Value::Object(_) | Value::Number(_) | Value::Bool(_) | Value::Null => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use wiremock::http::HeaderMap; + use wiremock::http::Method; + + fn request_with_input(input: Value) -> ResponsesRequest { + ResponsesRequest(wiremock::Request { + url: "http://localhost/v1/responses" + .parse() + .expect("valid request url"), + method: Method::POST, + headers: HeaderMap::new(), + body: serde_json::to_vec(&serde_json::json!({ "input": input })) + .expect("serialize request body"), + }) + } + + #[test] + fn call_output_content_and_success_returns_only_single_text_content_item() { + let single_text = request_with_input(serde_json::json!([ + { + "type": "function_call_output", + "call_id": "call-1", + "output": [{ "type": "input_text", "text": "hello" }] + }, + { + "type": "custom_tool_call_output", + "call_id": "call-2", + "output": [{ "type": "input_text", "text": "world" }] + } + ])); + assert_eq!( + single_text.function_call_output_content_and_success("call-1"), + Some((Some("hello".to_string()), None)) + ); + assert_eq!( + single_text.custom_tool_call_output_content_and_success("call-2"), + Some((Some("world".to_string()), None)) + ); + + let mixed_content = request_with_input(serde_json::json!([ + { + "type": "function_call_output", + "call_id": "call-3", + "output": [ + { "type": "input_text", "text": "hello" }, + { "type": "input_image", "image_url": "data:image/png;base64,abc" } + ] + }, + { + "type": "custom_tool_call_output", + "call_id": "call-4", + "output": [{ "type": "input_image", "image_url": "data:image/png;base64,abc" }] + } + ])); + assert_eq!( + mixed_content.function_call_output_content_and_success("call-3"), + Some((None, None)) + ); + assert_eq!( + mixed_content.custom_tool_call_output_content_and_success("call-4"), + Some((None, None)) + ); + } +} + #[derive(Debug, Clone)] pub struct WebSocketRequest { body: Value, @@ -309,6 +417,7 @@ pub struct WebSocketTestServer { uri: String, connections: Arc>>>, handshakes: Arc>>, + request_log_updated: Arc, shutdown: oneshot::Sender<()>, task: tokio::task::JoinHandle<()>, } @@ -330,6 +439,26 @@ impl WebSocketTestServer { connections.first().cloned().unwrap_or_default() } + pub async fn wait_for_request( + &self, + connection_index: usize, + request_index: usize, + ) -> WebSocketRequest { + loop { + if let Some(request) = self + .connections + .lock() + .unwrap() + .get(connection_index) + .and_then(|connection| connection.get(request_index)) + .cloned() + { + return request; + } + self.request_log_updated.notified().await; + } + } + pub fn handshakes(&self) -> Vec { self.handshakes.lock().unwrap().clone() } @@ -1043,6 +1172,7 @@ pub async fn start_websocket_server(connections: Vec>>) -> WebSoc pub async fn start_websocket_server_with_headers( connections: Vec, ) -> WebSocketTestServer { + let start = std::time::Instant::now(); let listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind websocket server"); @@ -1050,8 +1180,10 @@ pub async fn start_websocket_server_with_headers( let uri = format!("ws://{addr}"); let connections_log = Arc::new(Mutex::new(Vec::new())); let handshakes_log = Arc::new(Mutex::new(Vec::new())); + let request_log_updated = Arc::new(Notify::new()); let requests = Arc::clone(&connections_log); let handshakes = Arc::clone(&handshakes_log); + let request_log = Arc::clone(&request_log_updated); let connections = Arc::new(Mutex::new(VecDeque::from(connections))); let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); @@ -1133,9 +1265,51 @@ pub async fn start_websocket_server_with_headers( let mut log = requests.lock().unwrap(); if let Some(connection_log) = log.get_mut(connection_index) { connection_log.push(WebSocketRequest { body }); + let request_index = connection_log.len() - 1; + let request = &connection_log[request_index]; + let request_body = request.body_json(); + eprintln!( + "[ws test server +{}ms] connection={} received request={} type={:?} role={:?} text={:?} data={:?}", + start.elapsed().as_millis(), + connection_index, + request_index, + request_body.get("type").and_then(Value::as_str), + request_body + .get("item") + .and_then(|item| item.get("role")) + .and_then(Value::as_str), + request_body + .get("item") + .and_then(|item| item.get("content")) + .and_then(Value::as_array) + .and_then(|content| content.first()) + .and_then(|content| content.get("text")) + .and_then(Value::as_str), + request_body + .get("item") + .and_then(|item| item.get("content")) + .and_then(Value::as_array) + .and_then(|content| content.first()) + .and_then(|content| content.get("data")) + .and_then(Value::as_str), + ); } + request_log.notify_waiters(); } + eprintln!( + "[ws test server +{}ms] connection={} sending batch_size={} event_types={:?} audio_data={:?}", + start.elapsed().as_millis(), + connection_index, + request_events.len(), + request_events + .iter() + .map(|event| event.get("type").and_then(Value::as_str)) + .collect::>(), + request_events + .iter() + .find_map(|event| event.get("delta").and_then(Value::as_str)), + ); for event in &request_events { let Ok(payload) = serde_json::to_string(event) else { continue; @@ -1158,6 +1332,7 @@ pub async fn start_websocket_server_with_headers( uri, connections: connections_log, handshakes: handshakes_log, + request_log_updated, shutdown: shutdown_tx, task, } diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 173db5ca77..bd15c6d7e4 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -11,7 +11,6 @@ use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::config::Config; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -24,6 +23,7 @@ use wiremock::MockServer; use crate::load_default_config_for_test; use crate::responses::WebSocketTestServer; +use crate::responses::output_value_to_text; use crate::responses::start_mock_server; use crate::streaming_sse::StreamingSseServer; use crate::wait_for_event; @@ -300,7 +300,7 @@ impl TestCodex { sandbox_policy, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -395,11 +395,7 @@ impl TestCodexHarness { pub async fn custom_tool_call_output(&self, call_id: &str) -> String { let bodies = self.request_bodies().await; - custom_tool_call_output(&bodies, call_id) - .get("output") - .and_then(Value::as_str) - .expect("output string") - .to_string() + custom_tool_call_output_text(&bodies, call_id) } pub async fn apply_patch_output( @@ -434,6 +430,14 @@ fn custom_tool_call_output<'a>(bodies: &'a [Value], call_id: &str) -> &'a Value panic!("custom_tool_call_output {call_id} not found"); } +fn custom_tool_call_output_text(bodies: &[Value], call_id: &str) -> String { + let output = custom_tool_call_output(bodies, call_id) + .get("output") + .unwrap_or_else(|| panic!("custom_tool_call_output {call_id} missing output")); + output_value_to_text(output) + .unwrap_or_else(|| panic!("custom_tool_call_output {call_id} missing text output")) +} + fn function_call_output<'a>(bodies: &'a [Value], call_id: &str) -> &'a Value { for body in bodies { if let Some(items) = body.get("input").and_then(Value::as_array) { @@ -457,3 +461,36 @@ pub fn test_codex() -> TestCodexBuilder { home: None, } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn custom_tool_call_output_text_returns_output_text() { + let bodies = vec![json!({ + "input": [{ + "type": "custom_tool_call_output", + "call_id": "call-1", + "output": "hello" + }] + })]; + + assert_eq!(custom_tool_call_output_text(&bodies, "call-1"), "hello"); + } + + #[test] + #[should_panic(expected = "custom_tool_call_output call-2 missing output")] + fn custom_tool_call_output_text_panics_when_output_is_missing() { + let bodies = vec![json!({ + "input": [{ + "type": "custom_tool_call_output", + "call_id": "call-2" + }] + })]; + + let _ = custom_tool_call_output_text(&bodies, "call-2"); + } +} diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 126dc2c288..f502ec08be 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -111,7 +111,14 @@ async fn responses_stream_includes_subagent_header_on_review() { }]; let mut stream = client_session - .stream(&prompt, &model_info, &otel_manager, effort, summary, None) + .stream( + &prompt, + &model_info, + &otel_manager, + effort, + summary.unwrap_or(model_info.default_reasoning_summary), + None, + ) .await .expect("stream failed"); while let Some(event) = stream.next().await { @@ -216,7 +223,14 @@ async fn responses_stream_includes_subagent_header_on_other() { }]; let mut stream = client_session - .stream(&prompt, &model_info, &otel_manager, effort, summary, None) + .stream( + &prompt, + &model_info, + &otel_manager, + effort, + summary.unwrap_or(model_info.default_reasoning_summary), + None, + ) .await .expect("stream failed"); while let Some(event) = stream.next().await { @@ -267,7 +281,7 @@ async fn responses_respects_model_info_overrides_from_config() { config.model_provider_id = provider.name.clone(); config.model_provider = provider.clone(); config.model_supports_reasoning_summaries = Some(true); - config.model_reasoning_summary = ReasoningSummary::Detailed; + config.model_reasoning_summary = Some(ReasoningSummary::Detailed); let effort = config.model_reasoning_effort; let summary = config.model_reasoning_summary; let model = config.model.clone().expect("model configured"); @@ -320,7 +334,14 @@ async fn responses_respects_model_info_overrides_from_config() { }]; let mut stream = client_session - .stream(&prompt, &model_info, &otel_manager, effort, summary, None) + .stream( + &prompt, + &model_info, + &otel_manager, + effort, + summary.unwrap_or(model_info.default_reasoning_summary), + None, + ) .await .expect("stream failed"); while let Some(event) = stream.next().await { diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 9ef2715d8c..7f20f468d9 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -1,6 +1,7 @@ #![allow(clippy::expect_used)] use anyhow::Result; +use codex_test_macros::large_stack_test; use core_test_support::responses::ev_apply_patch_call; use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_shell_command_call; @@ -11,7 +12,6 @@ use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -85,7 +85,7 @@ fn apply_patch_responses( ] } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -132,7 +132,7 @@ D delete.txt Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -159,7 +159,7 @@ async fn apply_patch_cli_multiple_chunks(model_output: ApplyPatchModelOutput) -> Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -188,7 +188,7 @@ async fn apply_patch_cli_moves_file_to_new_directory( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -216,7 +216,7 @@ async fn apply_patch_cli_updates_file_appends_trailing_newline( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -242,7 +242,7 @@ async fn apply_patch_cli_insert_only_hunk_modifies_file( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -273,7 +273,7 @@ async fn apply_patch_cli_move_overwrites_existing_destination( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -311,7 +311,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -334,7 +334,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -360,7 +360,7 @@ async fn apply_patch_cli_add_overwrites_existing_file( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -392,7 +392,7 @@ async fn apply_patch_cli_rejects_invalid_hunk_header( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -426,7 +426,7 @@ async fn apply_patch_cli_reports_missing_context( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -462,7 +462,7 @@ async fn apply_patch_cli_reports_missing_target_file( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -499,7 +499,7 @@ async fn apply_patch_cli_delete_missing_file_reports_error( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -524,7 +524,7 @@ async fn apply_patch_cli_rejects_empty_patch(model_output: ApplyPatchModelOutput Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -551,7 +551,7 @@ async fn apply_patch_cli_delete_directory_reports_verification_error( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -605,7 +605,7 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -660,7 +660,7 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -694,7 +694,7 @@ async fn apply_patch_cli_verification_failure_has_no_side_effects( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_shell_command_heredoc_with_cd_updates_relative_workdir() -> Result<()> { skip_if_no_network!(Ok(())); @@ -732,7 +732,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_updates_relative_workdir() -> Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result<()> { skip_if_no_network!(Ok(())); @@ -862,7 +862,7 @@ async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<()> { skip_if_no_network!(Ok(())); @@ -906,7 +906,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -945,7 +945,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> Result<()> { skip_if_no_network!(Ok(())); @@ -986,7 +986,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1021,7 +1021,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::ShellViaHeredoc)] #[test_case(ApplyPatchModelOutput::ShellCommandViaHeredoc)] async fn apply_patch_function_accepts_lenient_heredoc_wrapped_patch( @@ -1044,7 +1044,7 @@ async fn apply_patch_function_accepts_lenient_heredoc_wrapped_patch( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -1067,7 +1067,7 @@ async fn apply_patch_cli_end_of_file_anchor(model_output: ApplyPatchModelOutput) Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -1102,7 +1102,7 @@ async fn apply_patch_cli_missing_second_chunk_context_rejected( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -1136,7 +1136,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1161,7 +1161,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] @@ -1199,7 +1199,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1227,7 +1227,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1270,7 +1270,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1295,7 +1295,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1341,7 +1341,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1385,7 +1385,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[large_stack_test] #[test_case(ApplyPatchModelOutput::Freeform)] #[test_case(ApplyPatchModelOutput::Function)] #[test_case(ApplyPatchModelOutput::Shell)] diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index cbe178093c..a0530809db 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -13,7 +13,6 @@ use codex_core::sandboxing::SandboxPermissions; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -80,6 +79,10 @@ enum ActionKind { target: TargetPath, content: &'static str, }, + FetchUrlNoProxy { + endpoint: &'static str, + response_body: &'static str, + }, FetchUrl { endpoint: &'static str, response_body: &'static str, @@ -142,6 +145,28 @@ impl ActionKind { let event = shell_event(call_id, &command, 5_000, sandbox_permissions)?; Ok((event, Some(command))) } + ActionKind::FetchUrlNoProxy { + endpoint, + response_body, + } => { + Mock::given(method("GET")) + .and(path(*endpoint)) + .respond_with( + ResponseTemplate::new(200).set_body_string(response_body.to_string()), + ) + .mount(server) + .await; + + let url = format!("{}{}", server.uri(), endpoint); + let escaped_url = url.replace('\'', "\\'"); + let script = format!( + "import sys\nimport urllib.request\nurl = '{escaped_url}'\nopener = urllib.request.build_opener(urllib.request.ProxyHandler({{}}))\ntry:\n data = opener.open(url, timeout=2).read().decode()\n print('OK:' + data.strip())\nexcept Exception as exc:\n print('ERR:' + exc.__class__.__name__)\n sys.exit(1)", + ); + + let command = format!("python3 -c \"{script}\""); + let event = shell_event(call_id, &command, 5_000, sandbox_permissions)?; + Ok((event, Some(command))) + } ActionKind::RunCommand { command } => { let event = shell_event(call_id, command, 1_000, sandbox_permissions)?; Ok((event, Some(command.to_string()))) @@ -525,7 +550,7 @@ async fn submit_turn( sandbox_policy, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -698,7 +723,7 @@ fn scenarios() -> Vec { name: "danger_full_access_on_request_allows_network", approval_policy: OnRequest, sandbox_policy: SandboxPolicy::DangerFullAccess, - action: ActionKind::FetchUrl { + action: ActionKind::FetchUrlNoProxy { endpoint: "/dfa/network", response_body: "danger-network-ok", }, @@ -714,7 +739,7 @@ fn scenarios() -> Vec { name: "danger_full_access_on_request_allows_network_gpt_5_1_no_exit", approval_policy: OnRequest, sandbox_policy: SandboxPolicy::DangerFullAccess, - action: ActionKind::FetchUrl { + action: ActionKind::FetchUrlNoProxy { endpoint: "/dfa/network", response_body: "danger-network-ok", }, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5fc94a033e..1de0522cb5 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -12,6 +12,7 @@ use codex_core::built_in_model_providers; use codex_core::default_client::originator; use codex_core::error::CodexErr; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_otel::OtelManager; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; @@ -30,9 +31,14 @@ use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction; +use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::RolloutLine; +use codex_protocol::protocol::SessionMeta; +use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::AppsTestServer; @@ -73,40 +79,14 @@ fn assert_message_role(request_body: &serde_json::Value, role: &str) { assert_eq!(request_body["role"].as_str().unwrap(), role); } -#[expect(clippy::expect_used)] -fn assert_message_equals(request_body: &serde_json::Value, text: &str) { - let content = request_body["content"][0]["text"] - .as_str() - .expect("invalid message content"); - - assert_eq!( - content, text, - "expected message content '{content}' to equal '{text}'" - ); -} - -#[expect(clippy::expect_used)] -fn assert_message_starts_with(request_body: &serde_json::Value, text: &str) { - let content = request_body["content"][0]["text"] - .as_str() - .expect("invalid message content"); - - assert!( - content.starts_with(text), - "expected message content '{content}' to start with '{text}'" - ); -} - -#[expect(clippy::expect_used)] -fn assert_message_ends_with(request_body: &serde_json::Value, text: &str) { - let content = request_body["content"][0]["text"] - .as_str() - .expect("invalid message content"); - - assert!( - content.ends_with(text), - "expected message content '{content}' to end with '{text}'" - ); +#[expect(clippy::unwrap_used)] +fn message_input_texts(item: &serde_json::Value) -> Vec<&str> { + item["content"] + .as_array() + .unwrap() + .iter() + .filter_map(|entry| entry.get("text").and_then(|text| text.as_str())) + .collect() } /// Writes an `auth.json` into the provided `codex_home` with the specified parameters. @@ -304,19 +284,15 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { let request = resp_mock.single_request(); let request_body = request.body_json(); let input = request_body["input"].as_array().expect("input array"); - let messages: Vec<(String, String)> = input - .iter() - .filter_map(|item| { - let role = item.get("role")?.as_str()?; - let text = item - .get("content")? - .as_array()? - .first()? - .get("text")? - .as_str()?; - Some((role.to_string(), text.to_string())) - }) - .collect(); + let mut messages: Vec<(String, String)> = Vec::new(); + for item in input { + let Some(role) = item.get("role").and_then(|role| role.as_str()) else { + continue; + }; + for text in message_input_texts(item) { + messages.push((role.to_string(), text.to_string())); + } + } let pos_prior_user = messages .iter() .position(|(role, text)| role == "user" && text == "resumed user message") @@ -353,8 +329,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { .position(|(role, text)| { role == "user" && text.contains("be nice") - && (text.starts_with("# AGENTS.md instructions for ") - || text.starts_with("")) + && (text.starts_with("# AGENTS.md instructions for ")) }) .expect("user instructions"); let pos_environment = messages @@ -373,6 +348,144 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { assert!(pos_environment < pos_new_user); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn resume_replays_legacy_js_repl_image_rollout_shapes() { + skip_if_no_network!(); + + // Early js_repl builds persisted image tool results as two separate rollout items: + // a string-valued custom_tool_call_output plus a standalone user input_image message. + // Current image tests cover today's shapes; this keeps resume compatibility for that + // legacy rollout representation. + let legacy_custom_tool_call = ResponseItem::CustomToolCall { + id: None, + status: None, + call_id: "legacy-js-call".to_string(), + name: "js_repl".to_string(), + input: "console.log('legacy image flow')".to_string(), + }; + let legacy_image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; + let rollout = vec![ + RolloutLine { + timestamp: "2024-01-01T00:00:00.000Z".to_string(), + item: RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + id: ThreadId::default(), + timestamp: "2024-01-01T00:00:00Z".to_string(), + cwd: ".".into(), + originator: "test_originator".to_string(), + cli_version: "test_version".to_string(), + model_provider: Some("test-provider".to_string()), + ..Default::default() + }, + git: None, + }), + }, + RolloutLine { + timestamp: "2024-01-01T00:00:01.000Z".to_string(), + item: RolloutItem::ResponseItem(legacy_custom_tool_call), + }, + RolloutLine { + timestamp: "2024-01-01T00:00:02.000Z".to_string(), + item: RolloutItem::ResponseItem(ResponseItem::CustomToolCallOutput { + call_id: "legacy-js-call".to_string(), + output: FunctionCallOutputPayload::from_text("legacy js_repl stdout".to_string()), + }), + }, + RolloutLine { + timestamp: "2024-01-01T00:00:03.000Z".to_string(), + item: RolloutItem::ResponseItem(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputImage { + image_url: legacy_image_url.to_string(), + }], + end_turn: None, + phase: None, + }), + }, + ]; + + let tmpdir = TempDir::new().unwrap(); + let session_path = tmpdir + .path() + .join("resume-legacy-js-repl-image-rollout.jsonl"); + let mut f = std::fs::File::create(&session_path).unwrap(); + for line in rollout { + writeln!(f, "{}", serde_json::to_string(&line).unwrap()).unwrap(); + } + + let server = MockServer::start().await; + let resp_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + + let codex_home = Arc::new(TempDir::new().unwrap()); + let mut builder = test_codex().with_model("gpt-5.1"); + let test = builder + .resume(&server, codex_home, session_path.clone()) + .await + .expect("resume conversation"); + test.submit_turn("after resume").await.unwrap(); + + let input = resp_mock.single_request().input(); + + let legacy_output_index = input + .iter() + .position(|item| { + item.get("type").and_then(|value| value.as_str()) == Some("custom_tool_call_output") + && item.get("call_id").and_then(|value| value.as_str()) == Some("legacy-js-call") + }) + .expect("legacy custom tool output should be replayed"); + assert_eq!( + input[legacy_output_index] + .get("output") + .and_then(|value| value.as_str()), + Some("legacy js_repl stdout") + ); + + let legacy_image_index = input + .iter() + .position(|item| { + item.get("type").and_then(|value| value.as_str()) == Some("message") + && item.get("role").and_then(|value| value.as_str()) == Some("user") + && item + .get("content") + .and_then(|value| value.as_array()) + .is_some_and(|content| { + content.iter().any(|entry| { + entry.get("type").and_then(|value| value.as_str()) + == Some("input_image") + && entry.get("image_url").and_then(|value| value.as_str()) + == Some(legacy_image_url) + }) + }) + }) + .expect("legacy injected image message should be replayed"); + + let new_user_index = input + .iter() + .position(|item| { + item.get("type").and_then(|value| value.as_str()) == Some("message") + && item.get("role").and_then(|value| value.as_str()) == Some("user") + && item + .get("content") + .and_then(|value| value.as_array()) + .is_some_and(|content| { + content.iter().any(|entry| { + entry.get("type").and_then(|value| value.as_str()) == Some("input_text") + && entry.get("text").and_then(|value| value.as_str()) + == Some("after resume") + }) + }) + }) + .expect("new user message should be present"); + + assert!(legacy_output_index < new_user_index); + assert!(legacy_image_index < new_user_index); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn includes_conversation_id_and_model_headers_in_request() { skip_if_no_network!(); @@ -584,6 +697,11 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { auth_manager, SessionSource::Exec, config.model_catalog.clone(), + CollaborationModesConfig { + default_mode_request_user_input: config + .features + .enabled(Feature::DefaultModeRequestUserInput), + }, ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) @@ -658,16 +776,27 @@ async fn includes_user_instructions_message_in_request() { ); assert_message_role(&request_body["input"][1], "user"); - assert_message_starts_with(&request_body["input"][1], "# AGENTS.md instructions for "); - assert_message_ends_with(&request_body["input"][1], ""); - let ui_text = request_body["input"][1]["content"][0]["text"] - .as_str() + let user_context_texts = message_input_texts(&request_body["input"][1]); + assert!( + user_context_texts + .iter() + .any(|text| text.starts_with("# AGENTS.md instructions for ")), + "expected AGENTS text in contextual user message, got {user_context_texts:?}" + ); + let ui_text = user_context_texts + .iter() + .copied() + .find(|text| text.contains("")) .expect("invalid message content"); assert!(ui_text.contains("")); assert!(ui_text.contains("be nice")); - assert_message_role(&request_body["input"][2], "user"); - assert_message_starts_with(&request_body["input"][2], ""); - assert_message_ends_with(&request_body["input"][2], ""); + assert!( + user_context_texts + .iter() + .any(|text| text.starts_with("") + && text.ends_with("")), + "expected environment context in contextual user message, got {user_context_texts:?}" + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -721,10 +850,14 @@ async fn includes_apps_guidance_as_developer_message_when_enabled() { && item .get("content") .and_then(|value| value.as_array()) - .and_then(|value| value.first()) - .and_then(|value| value.get("text")) - .and_then(|value| value.as_str()) - .is_some_and(|text| text.contains(apps_snippet)) + .is_some_and(|content| { + content.iter().any(|entry| { + entry + .get("text") + .and_then(|value| value.as_str()) + .is_some_and(|text| text.contains(apps_snippet)) + }) + }) }); assert!( has_developer_apps_guidance, @@ -736,10 +869,14 @@ async fn includes_apps_guidance_as_developer_message_when_enabled() { && item .get("content") .and_then(|value| value.as_array()) - .and_then(|value| value.first()) - .and_then(|value| value.get("text")) - .and_then(|value| value.as_str()) - .is_some_and(|text| text.contains(apps_snippet)) + .is_some_and(|content| { + content.iter().any(|entry| { + entry + .get("text") + .and_then(|value| value.as_str()) + .is_some_and(|text| text.contains(apps_snippet)) + }) + }) }); assert!( !has_user_apps_guidance, @@ -986,7 +1123,11 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: session_configured.model.clone(), effort: Some(ReasoningEffort::Low), - summary: config.model_reasoning_summary, + summary: Some( + config + .model_reasoning_summary + .unwrap_or(ReasoningSummary::Auto), + ), collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, @@ -1020,7 +1161,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { .await; let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.model_reasoning_summary = ReasoningSummary::Concise; + config.model_reasoning_summary = Some(ReasoningSummary::Concise); }) .build(&server) .await?; @@ -1052,6 +1193,75 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() -> anyhow::Result<()> +{ + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + + let mut model_catalog: ModelsResponse = + serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + let model = model_catalog + .models + .iter_mut() + .find(|model| model.slug == "gpt-5.1") + .expect("gpt-5.1 exists in bundled models.json"); + model.supports_reasoning_summaries = true; + model.default_reasoning_summary = ReasoningSummary::Detailed; + + let TestCodex { + codex, + config, + session_configured, + .. + } = test_codex() + .with_model("gpt-5.1") + .with_config(move |config| { + config.model_catalog = Some(model_catalog); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + cwd: config.cwd.clone(), + approval_policy: config.permissions.approval_policy.value(), + sandbox_policy: config.permissions.sandbox_policy.get().clone(), + model: session_configured.model, + effort: None, + summary: Some(ReasoningSummary::Concise), + collaboration_mode: None, + final_output_json_schema: None, + personality: None, + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let request_body = resp_mock.single_request().body_json(); + + pretty_assertions::assert_eq!( + request_body + .get("reasoning") + .and_then(|reasoning| reasoning.get("summary")) + .and_then(|value| value.as_str()), + Some("concise") + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -1064,7 +1274,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { .await; let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.model_reasoning_summary = ReasoningSummary::None; + config.model_reasoning_summary = Some(ReasoningSummary::None); }) .build(&server) .await?; @@ -1095,6 +1305,60 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = mount_sse_once( + &server, + sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), + ) + .await; + + let mut model_catalog: ModelsResponse = + serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + let model = model_catalog + .models + .iter_mut() + .find(|model| model.slug == "gpt-5.1") + .expect("gpt-5.1 exists in bundled models.json"); + model.supports_reasoning_summaries = true; + model.default_reasoning_summary = ReasoningSummary::Detailed; + + let TestCodex { codex, .. } = test_codex() + .with_model("gpt-5.1") + .with_config(move |config| { + config.model_reasoning_summary = Some(ReasoningSummary::None); + config.model_catalog = Some(model_catalog); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let request_body = resp_mock.single_request().body_json(); + pretty_assertions::assert_eq!( + request_body + .get("reasoning") + .and_then(|reasoning| reasoning.get("summary")), + None + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -1277,19 +1541,42 @@ async fn includes_developer_instructions_message_in_request() { "expected permissions message to mention sandbox_mode, got {permissions_text:?}" ); - assert_message_role(&request_body["input"][1], "developer"); - assert_message_equals(&request_body["input"][1], "be useful"); - assert_message_role(&request_body["input"][2], "user"); - assert_message_starts_with(&request_body["input"][2], "# AGENTS.md instructions for "); - assert_message_ends_with(&request_body["input"][2], ""); - let ui_text = request_body["input"][2]["content"][0]["text"] - .as_str() + let developer_messages: Vec<&serde_json::Value> = request_body["input"] + .as_array() + .expect("input array") + .iter() + .filter(|item| item.get("role").and_then(|role| role.as_str()) == Some("developer")) + .collect(); + assert!( + developer_messages + .iter() + .any(|item| message_input_texts(item).contains(&"be useful")), + "expected developer instructions in a developer message, got {:?}", + request_body["input"] + ); + + assert_message_role(&request_body["input"][1], "user"); + let user_context_texts = message_input_texts(&request_body["input"][1]); + assert!( + user_context_texts + .iter() + .any(|text| text.starts_with("# AGENTS.md instructions for ")), + "expected AGENTS text in contextual user message, got {user_context_texts:?}" + ); + let ui_text = user_context_texts + .iter() + .copied() + .find(|text| text.contains("")) .expect("invalid message content"); assert!(ui_text.contains("")); assert!(ui_text.contains("be nice")); - assert_message_role(&request_body["input"][3], "user"); - assert_message_starts_with(&request_body["input"][3], ""); - assert_message_ends_with(&request_body["input"][3], ""); + assert!( + user_context_texts + .iter() + .any(|text| text.starts_with("") + && text.ends_with("")), + "expected environment context in contextual user message, got {user_context_texts:?}" + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1420,11 +1707,18 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { }); prompt.input.push(ResponseItem::CustomToolCallOutput { call_id: "custom-tool-call-id".into(), - output: "ok".into(), + output: FunctionCallOutputPayload::from_text("ok".into()), }); let mut stream = client_session - .stream(&prompt, &model_info, &otel_manager, effort, summary, None) + .stream( + &prompt, + &model_info, + &otel_manager, + effort, + summary.unwrap_or(ReasoningSummary::Auto), + None, + ) .await .expect("responses stream to start"); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 3dec0c4408..8ded6da3e7 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -300,7 +300,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn responses_websocket_prewarm_uses_model_preference_when_feature_disabled() { +async fn responses_websocket_prewarm_uses_v2_when_model_prefers_websockets_and_feature_disabled() { skip_if_no_network!(); let server = start_websocket_server(vec![vec![vec![ @@ -324,20 +324,40 @@ async fn responses_websocket_prewarm_uses_model_preference_when_feature_disabled .await .expect("websocket prewarm failed"); - // V1 prewarm only preconnects and should not issue a request. + // V2 prewarm issues a request on the websocket connection. assert_eq!(server.handshakes().len(), 1); - assert_eq!(server.single_connection().len(), 0); + assert_eq!(server.single_connection().len(), 1); + + let handshake = server.single_handshake(); + let openai_beta_header = handshake + .header(OPENAI_BETA_HEADER) + .expect("missing OpenAI-Beta header"); + assert!( + openai_beta_header + .split(',') + .map(str::trim) + .any(|value| value == WS_V2_BETA_HEADER_VALUE) + ); + assert!( + !openai_beta_header + .split(',') + .map(str::trim) + .any(|value| value == OPENAI_BETA_RESPONSES_WEBSOCKETS) + ); stream_until_complete(&mut client_session, &harness, &prompt).await; assert_eq!(server.handshakes().len(), 1); let connection = server.single_connection(); assert_eq!(connection.len(), 1); - let turn = connection + let prewarm = connection .first() - .expect("missing turn request") + .expect("missing prewarm request") .body_json(); - assert_eq!(turn["type"].as_str(), Some("response.create")); - assert_eq!(turn["input"], serde_json::to_value(&prompt.input).unwrap()); + assert_eq!(prewarm["type"].as_str(), Some("response.create")); + assert_eq!( + prewarm["input"], + serde_json::to_value(&prompt.input).unwrap() + ); server.shutdown().await; } @@ -923,6 +943,44 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() { server.shutdown().await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_websocket_connection_limit_error_reconnects_and_completes() { + skip_if_no_network!(); + + let websocket_connection_limit_error = json!({ + "type": "error", + "status": 400, + "error": { + "type": "invalid_request_error", + "code": "websocket_connection_limit_reached", + "message": "Responses websocket connection limit reached (60 minutes). Create a new websocket connection to continue." + } + }); + + let server = start_websocket_server(vec![ + vec![vec![websocket_connection_limit_error]], + vec![vec![ev_response_created("resp-1"), ev_completed("resp-1")]], + ]) + .await; + let mut builder = test_codex().with_config(|config| { + config.model_provider.request_max_retries = Some(0); + config.model_provider.stream_max_retries = Some(1); + }); + let test = builder + .build_with_websocket_server(&server) + .await + .expect("build websocket codex"); + + test.submit_turn("hello") + .await + .expect("submission should reconnect after websocket connection limit error"); + + let total_websocket_requests: usize = server.connections().iter().map(Vec::len).sum(); + assert_eq!(total_websocket_requests, 2); + + server.shutdown().await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn responses_websocket_appends_on_prefix() { skip_if_no_network!(); diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 68d695325d..b1dbd2d660 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -169,7 +169,11 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), model: test.session_configured.model.clone(), effort: None, - summary: test.config.model_reasoning_summary, + summary: Some( + test.config + .model_reasoning_summary + .unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto), + ), collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, @@ -275,7 +279,11 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), model: test.session_configured.model.clone(), effort: None, - summary: test.config.model_reasoning_summary, + summary: Some( + test.config + .model_reasoning_summary + .unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto), + ), collaboration_mode: Some(turn_mode), final_output_json_schema: None, personality: None, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 9e9ce931e9..9e376ce494 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -5,7 +5,6 @@ use codex_core::built_in_model_providers; use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARY_PREFIX; use codex_core::config::Config; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::items::TurnItem; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; @@ -756,16 +755,40 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { let body = requests_payloads[0].body_json(); let input = body.get("input").and_then(|v| v.as_array()).unwrap(); + fn strip_agents_parts_from_user_message( + value: &serde_json::Value, + ) -> Option { + let content = value + .get("content") + .and_then(|content| content.as_array())?; + let filtered_content = content + .iter() + .filter(|item| { + !item + .get("text") + .and_then(|text| text.as_str()) + .is_some_and(|text| text.starts_with("# AGENTS.md instructions for ")) + }) + .cloned() + .collect::>(); + if filtered_content.is_empty() { + return None; + } + let mut normalized = value.clone(); + normalized["content"] = serde_json::Value::Array(filtered_content); + Some(normalized) + } + fn normalize_inputs(values: &[serde_json::Value]) -> Vec { values .iter() - .filter(|value| { + .filter_map(|value| { if value .get("type") .and_then(|ty| ty.as_str()) .is_some_and(|ty| ty == "function_call_output") { - return false; + return None; } let text = value @@ -781,11 +804,13 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { if role == Some("developer") && text.is_some_and(|text| text.contains("`sandbox_mode`")) { - return false; + return None; } - !text.is_some_and(|text| text.starts_with("# AGENTS.md instructions for ")) + if role == Some("user") { + return strip_agents_parts_from_user_message(value); + } + Some(value.clone()) }) - .cloned() .collect() } @@ -1633,7 +1658,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { sandbox_policy: SandboxPolicy::DangerFullAccess, model: resumed.session_configured.model.clone(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1722,7 +1747,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { sandbox_policy: SandboxPolicy::DangerFullAccess, model: previous_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1745,7 +1770,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { sandbox_policy: SandboxPolicy::DangerFullAccess, model: next_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1854,7 +1879,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { sandbox_policy: SandboxPolicy::DangerFullAccess, model: previous_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1901,7 +1926,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { sandbox_policy: SandboxPolicy::DangerFullAccess, model: next_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -3102,7 +3127,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch sandbox_policy: SandboxPolicy::DangerFullAccess, model: previous_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -3125,7 +3150,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch sandbox_policy: SandboxPolicy::DangerFullAccess, model: next_model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -3184,7 +3209,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { ]); let mut responses = vec![first_turn]; responses.extend( - (0..6).map(|_| { + (0..5).map(|_| { sse_failed( "compact-failed", "context_length_exceeded", diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 1d450f9b11..573f38930b 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -77,6 +77,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { ) .await?; let codex = harness.test().codex.clone(); + let session_id = harness.test().session_configured.session_id.to_string(); let responses_mock = responses::mount_sse_sequence( harness.server(), @@ -137,6 +138,10 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { compact_request.header("authorization").as_deref(), Some("Bearer Access Token") ); + assert_eq!( + compact_request.header("session_id").as_deref(), + Some(session_id.as_str()) + ); let compact_body = compact_request.body_json(); assert_eq!( compact_body.get("model").and_then(|v| v.as_str()), @@ -195,6 +200,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { ) .await?; let codex = harness.test().codex.clone(); + let session_id = harness.test().session_configured.session_id.to_string(); mount_sse_once( harness.server(), @@ -237,6 +243,13 @@ async fn remote_compact_runs_automatically() -> Result<()> { wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert!(message); assert_eq!(compact_mock.requests().len(), 1); + assert_eq!( + compact_mock + .single_request() + .header("session_id") + .as_deref(), + Some(session_id.as_str()) + ); let follow_up_request = responses_mock.single_request(); let follow_up_body = follow_up_request.body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY")); diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index b6f78267b6..6546600681 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -33,7 +33,6 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 2349d9f5fa..c8f1f2eaf1 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -1,5 +1,4 @@ use anyhow::Context; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; @@ -126,7 +125,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -208,7 +207,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 844c55c6bc..dfb5ac88b4 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -377,7 +377,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result< sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, model: session_configured.model.clone(), effort: None, - summary: codex_protocol::config_types::ReasoningSummary::Auto, + summary: None, collaboration_mode: Some(collaboration_mode), personality: None, }) @@ -452,7 +452,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, model: session_configured.model.clone(), effort: None, - summary: codex_protocol::config_types::ReasoningSummary::Auto, + summary: None, collaboration_mode: Some(collaboration_mode), personality: None, }) @@ -505,6 +505,314 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done() +-> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let TestCodex { + codex, + session_configured, + .. + } = test_codex().build(&server).await?; + + let added_text = "Intro outer-doc\n\n- Step 1plan-doc\n- Step 2\n\nOu", + "tro", + ]; + let full_message = format!("{added_text}{}", deltas.concat()); + + let mut events = vec![ + ev_response_created("resp-1"), + ev_message_item_added("msg-1", added_text), + ]; + for delta in deltas { + events.push(ev_output_text_delta(delta)); + } + events.push(ev_assistant_message("msg-1", &full_message)); + events.push(ev_completed("resp-1")); + mount_sse_once(&server, sse(events)).await; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: session_configured.model.clone(), + reasoning_effort: None, + developer_instructions: None, + }, + }; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "please plan with citations".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: std::env::current_dir()?, + approval_policy: codex_protocol::protocol::AskForApproval::Never, + sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + model: session_configured.model.clone(), + effort: None, + summary: None, + collaboration_mode: Some(collaboration_mode), + personality: None, + }) + .await?; + + let mut agent_started = None; + let mut agent_started_idx = None; + let mut agent_completed = None; + let mut agent_completed_idx = None; + let mut plan_started = None; + let mut plan_started_idx = None; + let mut plan_completed = None; + let mut plan_completed_idx = None; + let mut agent_deltas = Vec::new(); + let mut plan_deltas = Vec::new(); + let mut first_agent_delta_idx = None; + let mut last_agent_delta_idx = None; + let mut first_plan_delta_idx = None; + let mut last_plan_delta_idx = None; + let mut idx = 0usize; + + let turn_complete_idx = loop { + let ev = wait_for_event(&codex, |_| true).await; + match ev { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::AgentMessage(item), + .. + }) => { + agent_started_idx = Some(idx); + agent_started = Some(item); + } + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::Plan(item), + .. + }) => { + plan_started_idx = Some(idx); + plan_started = Some(item); + } + EventMsg::AgentMessageContentDelta(event) => { + if first_agent_delta_idx.is_none() { + first_agent_delta_idx = Some(idx); + } + last_agent_delta_idx = Some(idx); + agent_deltas.push(event.delta); + } + EventMsg::PlanDelta(event) => { + if first_plan_delta_idx.is_none() { + first_plan_delta_idx = Some(idx); + } + last_plan_delta_idx = Some(idx); + plan_deltas.push(event.delta); + } + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::AgentMessage(item), + .. + }) => { + agent_completed_idx = Some(idx); + agent_completed = Some(item); + } + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::Plan(item), + .. + }) => { + plan_completed_idx = Some(idx); + plan_completed = Some(item); + } + EventMsg::TurnComplete(_) => { + break idx; + } + _ => {} + } + idx += 1; + }; + + let agent_started = agent_started.expect("agent item start should be emitted"); + let agent_completed = agent_completed.expect("agent item completion should be emitted"); + let plan_started = plan_started.expect("plan item start should be emitted"); + let plan_completed = plan_completed.expect("plan item completion should be emitted"); + + assert_eq!(agent_started.id, agent_completed.id); + assert_eq!(plan_started.id, plan_completed.id); + assert_eq!(plan_started.text, ""); + + let agent_started_text: String = agent_started + .content + .iter() + .map(|entry| match entry { + AgentMessageContent::Text { text } => text.as_str(), + }) + .collect(); + let agent_completed_text: String = agent_completed + .content + .iter() + .map(|entry| match entry { + AgentMessageContent::Text { text } => text.as_str(), + }) + .collect(); + let agent_delta_text = agent_deltas.concat(); + let plan_delta_text = plan_deltas.concat(); + + assert_eq!(agent_started_text, ""); + assert_eq!(agent_delta_text, "Intro \nOutro"); + assert_eq!(agent_completed_text, "Intro \nOutro"); + assert_eq!(plan_delta_text, "- Step 1\n- Step 2\n"); + assert_eq!(plan_completed.text, "- Step 1\n- Step 2\n"); + + for text in [ + agent_started_text.as_str(), + agent_delta_text.as_str(), + agent_completed_text.as_str(), + plan_delta_text.as_str(), + plan_completed.text.as_str(), + ] { + assert!(!text.contains("")); + assert!(!text.contains("")); + } + + let agent_started_idx = agent_started_idx.expect("agent start index"); + let agent_completed_idx = agent_completed_idx.expect("agent completion index"); + let plan_started_idx = plan_started_idx.expect("plan start index"); + let plan_completed_idx = plan_completed_idx.expect("plan completion index"); + let first_agent_delta_idx = first_agent_delta_idx.expect("agent delta index"); + let last_agent_delta_idx = last_agent_delta_idx.expect("agent delta index"); + let first_plan_delta_idx = first_plan_delta_idx.expect("plan delta index"); + let last_plan_delta_idx = last_plan_delta_idx.expect("plan delta index"); + assert!(agent_started_idx < first_agent_delta_idx); + assert!(plan_started_idx < first_plan_delta_idx); + assert!(last_agent_delta_idx < agent_completed_idx); + assert!(last_plan_delta_idx < plan_completed_idx); + assert!(agent_completed_idx < turn_complete_idx); + assert!(plan_completed_idx < turn_complete_idx); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_parsed() +-> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let TestCodex { + codex, + session_configured, + .. + } = test_codex().build(&server).await?; + + let added_text = "Intro\n\n- Step 1\n\nOutro"]; + let full_message = format!("{added_text}{}", deltas.concat()); + + let mut events = vec![ + ev_response_created("resp-1"), + ev_message_item_added("msg-1", added_text), + ]; + for delta in deltas { + events.push(ev_output_text_delta(delta)); + } + events.push(ev_assistant_message("msg-1", &full_message)); + events.push(ev_completed("resp-1")); + mount_sse_once(&server, sse(events)).await; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: session_configured.model.clone(), + reasoning_effort: None, + developer_instructions: None, + }, + }; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "please plan".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: std::env::current_dir()?, + approval_policy: codex_protocol::protocol::AskForApproval::Never, + sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, + model: session_configured.model.clone(), + effort: None, + summary: None, + collaboration_mode: Some(collaboration_mode), + personality: None, + }) + .await?; + + let mut agent_started = None; + let mut agent_completed = None; + let mut plan_started = None; + let mut plan_completed = None; + let mut agent_deltas = Vec::new(); + let mut plan_deltas = Vec::new(); + + loop { + let ev = wait_for_event(&codex, |_| true).await; + match ev { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::AgentMessage(item), + .. + }) => agent_started = Some(item), + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::Plan(item), + .. + }) => plan_started = Some(item), + EventMsg::AgentMessageContentDelta(event) => agent_deltas.push(event.delta), + EventMsg::PlanDelta(event) => plan_deltas.push(event.delta), + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::AgentMessage(item), + .. + }) => agent_completed = Some(item), + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::Plan(item), + .. + }) => plan_completed = Some(item), + EventMsg::TurnComplete(_) => break, + _ => {} + } + } + + let agent_started = agent_started.expect("agent item start should be emitted"); + let agent_completed = agent_completed.expect("agent item completion should be emitted"); + let plan_started = plan_started.expect("plan item start should be emitted"); + let plan_completed = plan_completed.expect("plan item completion should be emitted"); + + let agent_started_text: String = agent_started + .content + .iter() + .map(|entry| match entry { + AgentMessageContent::Text { text } => text.as_str(), + }) + .collect(); + let agent_completed_text: String = agent_completed + .content + .iter() + .map(|entry| match entry { + AgentMessageContent::Text { text } => text.as_str(), + }) + .collect(); + + assert_eq!(agent_started_text, ""); + assert_eq!(agent_deltas.concat(), "Intro\nOutro"); + assert_eq!(agent_completed_text, "Intro\nOutro"); + assert_eq!(plan_started.text, ""); + assert_eq!(plan_deltas.concat(), "- Step 1\n"); + assert_eq!(plan_completed.text, "- Step 1\n"); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -548,7 +856,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, model: session_configured.model.clone(), effort: None, - summary: codex_protocol::config_types::ReasoningSummary::Auto, + summary: None, collaboration_mode: Some(collaboration_mode), personality: None, }) diff --git a/codex-rs/core/tests/suite/js_repl.rs b/codex-rs/core/tests/suite/js_repl.rs new file mode 100644 index 0000000000..31dbe6e60a --- /dev/null +++ b/codex-rs/core/tests/suite/js_repl.rs @@ -0,0 +1,339 @@ +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use anyhow::Result; +use codex_core::features::Feature; +use codex_protocol::protocol::EventMsg; +use core_test_support::responses; +use core_test_support::responses::ResponseMock; +use core_test_support::responses::ResponsesRequest; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_custom_tool_call; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::sse; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event_match; +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use tempfile::tempdir; +use wiremock::MockServer; + +fn custom_tool_output_text_and_success( + req: &ResponsesRequest, + call_id: &str, +) -> (String, Option) { + let (output, success) = req + .custom_tool_call_output_content_and_success(call_id) + .expect("custom tool output should be present"); + (output.unwrap_or_default(), success) +} + +fn tool_names(body: &serde_json::Value) -> Vec { + body["tools"] + .as_array() + .expect("tools array should be present") + .iter() + .map(|tool| { + tool.get("name") + .and_then(|value| value.as_str()) + .or_else(|| tool.get("type").and_then(|value| value.as_str())) + .expect("tool should have a name or type") + .to_string() + }) + .collect() +} + +fn write_too_old_node_script(dir: &Path) -> Result { + #[cfg(windows)] + { + let path = dir.join("old-node.cmd"); + fs::write(&path, "@echo off\r\necho v0.0.1\r\n")?; + Ok(path) + } + + #[cfg(unix)] + { + let path = dir.join("old-node.sh"); + fs::write(&path, "#!/bin/sh\necho v0.0.1\n")?; + let mut permissions = fs::metadata(&path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&path, permissions)?; + Ok(path) + } + + #[cfg(not(any(unix, windows)))] + { + anyhow::bail!("unsupported platform for js_repl test fixture"); + } +} + +async fn run_js_repl_turn( + server: &MockServer, + prompt: &str, + calls: &[(&str, &str)], +) -> Result { + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::JsRepl); + }); + let test = builder.build(server).await?; + + let mut first_events = vec![ev_response_created("resp-1")]; + for (call_id, js_input) in calls { + first_events.push(ev_custom_tool_call(call_id, "js_repl", js_input)); + } + first_events.push(ev_completed("resp-1")); + responses::mount_sse_once(server, sse(first_events)).await; + + let second_mock = responses::mount_sse_once( + server, + sse(vec![ + ev_assistant_message("msg-1", "done"), + ev_completed("resp-2"), + ]), + ) + .await; + + test.submit_turn(prompt).await?; + Ok(second_mock) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_is_not_advertised_when_startup_node_is_incompatible() -> Result<()> { + skip_if_no_network!(Ok(())); + if std::env::var_os("CODEX_JS_REPL_NODE_PATH").is_some() { + return Ok(()); + } + + let server = responses::start_mock_server().await; + let temp = tempdir()?; + let old_node = write_too_old_node_script(temp.path())?; + + let mut builder = test_codex().with_config(move |config| { + config.features.enable(Feature::JsRepl); + config.js_repl_node_path = Some(old_node); + }); + let test = builder.build(&server).await?; + let warning = wait_for_event_match(&test.codex, |event| match event { + EventMsg::Warning(ev) if ev.message.contains("Disabled `js_repl` for this session") => { + Some(ev.message.clone()) + } + _ => None, + }) + .await; + assert!( + warning.contains("Node runtime"), + "warning should explain the Node compatibility issue: {warning}" + ); + + let request_mock = responses::mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + + test.submit_turn("hello").await?; + + let body = request_mock.single_request().body_json(); + let tools = tool_names(&body); + assert!( + !tools.iter().any(|tool| tool == "js_repl"), + "js_repl should be omitted when startup validation fails: {tools:?}" + ); + assert!( + !tools.iter().any(|tool| tool == "js_repl_reset"), + "js_repl_reset should be omitted when startup validation fails: {tools:?}" + ); + let instructions = body["instructions"].as_str().unwrap_or_default(); + assert!( + !instructions.contains("## JavaScript REPL (Node)"), + "startup instructions should not mention js_repl when it is disabled: {instructions}" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_persists_top_level_bindings_and_supports_tla() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::JsRepl); + }); + let test = builder.build(&server).await?; + + responses::mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_custom_tool_call( + "call-1", + "js_repl", + "let x = await Promise.resolve(41); console.log(x);", + ), + ev_completed("resp-1"), + ]), + ) + .await; + let second_mock = responses::mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-2"), + ev_custom_tool_call("call-2", "js_repl", "console.log(x + 1);"), + ev_completed("resp-2"), + ]), + ) + .await; + let third_mock = responses::mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-1", "done"), + ev_completed("resp-3"), + ]), + ) + .await; + + test.submit_turn("run js_repl twice").await?; + + let req2 = second_mock.single_request(); + let (first_output, first_success) = custom_tool_output_text_and_success(&req2, "call-1"); + assert_ne!( + first_success, + Some(false), + "first js_repl call failed unexpectedly: {first_output}" + ); + assert!(first_output.contains("41")); + + let req3 = third_mock.single_request(); + let (second_output, second_success) = custom_tool_output_text_and_success(&req3, "call-2"); + assert_ne!( + second_success, + Some(false), + "second js_repl call failed unexpectedly: {second_output}" + ); + assert!(second_output.contains("42")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_can_invoke_builtin_tools() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let mock = run_js_repl_turn( + &server, + "use js_repl to call a tool", + &[( + "call-1", + "const toolOut = await codex.tool(\"list_mcp_resources\", {}); console.log(toolOut.type);", + )], + ) + .await?; + + let req = mock.single_request(); + let (output, success) = custom_tool_output_text_and_success(&req, "call-1"); + assert_ne!( + success, + Some(false), + "js_repl call failed unexpectedly: {output}" + ); + assert!(output.contains("function_call_output")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_tool_call_rejects_recursive_js_repl_invocation() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let mock = run_js_repl_turn( + &server, + "use js_repl recursively", + &[( + "call-1", + r#" +try { + await codex.tool("js_repl", "console.log('recursive')"); + console.log("unexpected-success"); +} catch (err) { + console.log(String(err)); +} +"#, + )], + ) + .await?; + + let req = mock.single_request(); + let (output, success) = custom_tool_output_text_and_success(&req, "call-1"); + assert_ne!( + success, + Some(false), + "js_repl call failed unexpectedly: {output}" + ); + assert!( + output.contains("js_repl cannot invoke itself"), + "expected recursion guard message, got output: {output}" + ); + assert!( + !output.contains("unexpected-success"), + "recursive js_repl call unexpectedly succeeded: {output}" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_does_not_expose_process_global() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let mock = run_js_repl_turn( + &server, + "check process visibility", + &[("call-1", "console.log(typeof process);")], + ) + .await?; + + let req = mock.single_request(); + let (output, success) = custom_tool_output_text_and_success(&req, "call-1"); + assert_ne!( + success, + Some(false), + "js_repl call failed unexpectedly: {output}" + ); + assert!(output.contains("undefined")); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn js_repl_blocks_sensitive_builtin_imports() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let mock = run_js_repl_turn( + &server, + "import a blocked module", + &[("call-1", "await import(\"node:process\");")], + ) + .await?; + + let req = mock.single_request(); + let (output, success) = custom_tool_output_text_and_success(&req, "call-1"); + assert_ne!( + success, + Some(true), + "blocked import unexpectedly succeeded: {output}" + ); + assert!(output.contains("Importing module \"node:process\" is not allowed in js_repl")); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index e32865e0c7..12c9845a75 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -1,6 +1,5 @@ #![cfg(not(target_os = "windows"))] -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -84,7 +83,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 3ea0a3cbe9..8ee4c56dda 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -7,7 +7,6 @@ use std::time::Duration; use anyhow::Result; use codex_core::config::ProjectConfig; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::TrustLevel; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -65,7 +64,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/memories.rs b/codex-rs/core/tests/suite/memories.rs new file mode 100644 index 0000000000..8f96178e86 --- /dev/null +++ b/codex-rs/core/tests/suite/memories.rs @@ -0,0 +1,289 @@ +use anyhow::Result; +use chrono::Duration as ChronoDuration; +use chrono::Utc; +use codex_core::features::Feature; +use codex_protocol::ThreadId; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::SessionSource; +use core_test_support::responses::ResponseMock; +use core_test_support::responses::ResponsesRequest; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use std::path::Path; +use std::sync::Arc; +use tempfile::TempDir; +use tokio::time::Duration; +use tokio::time::Instant; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn memories_startup_phase2_tracks_added_and_removed_inputs_across_runs() -> Result<()> { + let server = start_mock_server().await; + let home = Arc::new(TempDir::new()?); + let db = init_state_db(&home).await?; + + let now = Utc::now(); + let thread_a = seed_stage1_output( + db.as_ref(), + home.path(), + now - ChronoDuration::hours(2), + "raw memory A", + "rollout summary A", + "rollout-a", + ) + .await?; + + let first_phase2 = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-phase2-1"), + ev_assistant_message("msg-phase2-1", "phase2 complete"), + ev_completed("resp-phase2-1"), + ]), + ) + .await; + + let first = build_test_codex(&server, home.clone()).await?; + let first_request = wait_for_single_request(&first_phase2).await; + let first_prompt = phase2_prompt_text(&first_request); + assert!( + first_prompt.contains("- selected inputs this run: 1"), + "expected selected count in first prompt: {first_prompt}" + ); + assert!( + first_prompt.contains("- newly added since the last successful Phase 2 run: 1"), + "expected added count in first prompt: {first_prompt}" + ); + assert!( + first_prompt.contains("- removed from the last successful Phase 2 run: 0"), + "expected removed count in first prompt: {first_prompt}" + ); + assert!( + first_prompt.contains(&format!("- [added] thread_id={thread_a},")), + "expected thread A to be marked added: {first_prompt}" + ); + assert!( + first_prompt.contains("Removed from the last successful Phase 2 selection:\n- none"), + "expected no removed items in first prompt: {first_prompt}" + ); + + wait_for_phase2_success(db.as_ref(), thread_a).await?; + let memory_root = home.path().join("memories"); + let raw_memories = tokio::fs::read_to_string(memory_root.join("raw_memories.md")).await?; + assert!(raw_memories.contains("raw memory A")); + assert!(!raw_memories.contains("raw memory B")); + let rollout_summaries = read_rollout_summary_bodies(&memory_root).await?; + assert_eq!(rollout_summaries.len(), 1); + assert!(rollout_summaries[0].contains("rollout summary A")); + assert!(rollout_summaries[0].contains("git_branch: branch-rollout-a")); + + shutdown_test_codex(&first).await?; + + let thread_b = seed_stage1_output( + db.as_ref(), + home.path(), + now - ChronoDuration::hours(1), + "raw memory B", + "rollout summary B", + "rollout-b", + ) + .await?; + + let second_phase2 = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-phase2-2"), + ev_assistant_message("msg-phase2-2", "phase2 complete"), + ev_completed("resp-phase2-2"), + ]), + ) + .await; + + let second = build_test_codex(&server, home.clone()).await?; + let second_request = wait_for_single_request(&second_phase2).await; + let second_prompt = phase2_prompt_text(&second_request); + assert!( + second_prompt.contains("- selected inputs this run: 1"), + "expected selected count in second prompt: {second_prompt}" + ); + assert!( + second_prompt.contains("- newly added since the last successful Phase 2 run: 1"), + "expected added count in second prompt: {second_prompt}" + ); + assert!( + second_prompt.contains("- removed from the last successful Phase 2 run: 1"), + "expected removed count in second prompt: {second_prompt}" + ); + assert!( + second_prompt.contains(&format!("- [added] thread_id={thread_b},")), + "expected thread B to be marked added: {second_prompt}" + ); + assert!( + second_prompt.contains(&format!("- thread_id={thread_a},")), + "expected thread A to be marked removed: {second_prompt}" + ); + + wait_for_phase2_success(db.as_ref(), thread_b).await?; + let raw_memories = tokio::fs::read_to_string(memory_root.join("raw_memories.md")).await?; + assert!(raw_memories.contains("raw memory B")); + assert!(raw_memories.contains("raw memory A")); + let rollout_summaries = read_rollout_summary_bodies(&memory_root).await?; + assert_eq!(rollout_summaries.len(), 2); + assert!( + rollout_summaries + .iter() + .any(|summary| summary.contains("rollout summary B")) + ); + assert!( + rollout_summaries + .iter() + .any(|summary| summary.contains("git_branch: branch-rollout-b")) + ); + assert!( + rollout_summaries + .iter() + .any(|summary| summary.contains("rollout summary A")) + ); + + shutdown_test_codex(&second).await?; + Ok(()) +} + +async fn build_test_codex(server: &wiremock::MockServer, home: Arc) -> Result { + let mut builder = test_codex().with_home(home).with_config(|config| { + config.features.enable(Feature::Sqlite); + config.features.enable(Feature::MemoryTool); + config.memories.max_raw_memories_for_global = 1; + }); + builder.build(server).await +} + +async fn init_state_db(home: &Arc) -> Result> { + let db = + codex_state::StateRuntime::init(home.path().to_path_buf(), "test-provider".into(), None) + .await?; + db.mark_backfill_complete(None).await?; + Ok(db) +} + +async fn seed_stage1_output( + db: &codex_state::StateRuntime, + codex_home: &Path, + updated_at: chrono::DateTime, + raw_memory: &str, + rollout_summary: &str, + rollout_slug: &str, +) -> Result { + let thread_id = ThreadId::new(); + let mut metadata_builder = codex_state::ThreadMetadataBuilder::new( + thread_id, + codex_home.join(format!("rollout-{thread_id}.jsonl")), + updated_at, + SessionSource::Cli, + ); + metadata_builder.cwd = codex_home.join(format!("workspace-{rollout_slug}")); + metadata_builder.model_provider = Some("test-provider".to_string()); + metadata_builder.git_branch = Some(format!("branch-{rollout_slug}")); + let metadata = metadata_builder.build("test-provider"); + db.upsert_thread(&metadata).await?; + + let claim = db + .try_claim_stage1_job( + thread_id, + ThreadId::new(), + updated_at.timestamp(), + 3_600, + 64, + ) + .await?; + let ownership_token = match claim { + codex_state::Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage-1 claim outcome: {other:?}"), + }; + + assert!( + db.mark_stage1_job_succeeded( + thread_id, + &ownership_token, + updated_at.timestamp(), + raw_memory, + rollout_summary, + Some(rollout_slug), + ) + .await?, + "stage-1 success should enqueue global consolidation" + ); + + Ok(thread_id) +} + +async fn wait_for_single_request(mock: &ResponseMock) -> ResponsesRequest { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let requests = mock.requests(); + if let Some(request) = requests.into_iter().next() { + return request; + } + assert!( + Instant::now() < deadline, + "timed out waiting for phase2 request" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +#[allow(clippy::expect_used)] +fn phase2_prompt_text(request: &ResponsesRequest) -> String { + request + .message_input_texts("user") + .into_iter() + .find(|text| text.contains("Current selected Phase 1 inputs:")) + .expect("phase2 prompt text") +} + +async fn wait_for_phase2_success( + db: &codex_state::StateRuntime, + expected_thread_id: ThreadId, +) -> Result<()> { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let selection = db.get_phase2_input_selection(1, 30).await?; + if selection.selected.len() == 1 + && selection.selected[0].thread_id == expected_thread_id + && selection.retained_thread_ids == vec![expected_thread_id] + && selection.removed.is_empty() + { + return Ok(()); + } + + assert!( + Instant::now() < deadline, + "timed out waiting for phase2 success for {expected_thread_id}" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +async fn read_rollout_summary_bodies(memory_root: &Path) -> Result> { + let mut dir = tokio::fs::read_dir(memory_root.join("rollout_summaries")).await?; + let mut summaries = Vec::new(); + while let Some(entry) = dir.next_entry().await? { + summaries.push(tokio::fs::read_to_string(entry.path()).await?); + } + summaries.sort(); + Ok(summaries) +} + +async fn shutdown_test_codex(test: &TestCodex) -> Result<()> { + test.codex.submit(Op::Shutdown {}).await?; + wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; + Ok(()) +} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index f3b64e1e7d..e23fd9ddbb 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -78,14 +78,15 @@ mod grep_files; mod hierarchical_agents; mod image_rollout; mod items; +mod js_repl; mod json_result; mod list_dir; mod live_cli; mod live_reload; +mod memories; mod model_info_overrides; mod model_overrides; mod model_switching; -mod model_tools; mod model_visible_layout; mod models_cache_ttl; mod models_etag_responses; diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 7a92e99f1e..ee4afc36bf 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,4 +1,5 @@ use codex_core::CodexAuth; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; @@ -12,7 +13,12 @@ async fn offline_model_info_without_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1", &config).await; @@ -30,7 +36,12 @@ async fn offline_model_info_with_tool_output_override() { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); - let manager = ModelsManager::new(config.codex_home.clone(), auth_manager, None); + let manager = ModelsManager::new( + config.codex_home.clone(), + auth_manager, + None, + CollaborationModesConfig::default(), + ); let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 6e06b0f224..62d0545462 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -58,7 +58,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -91,7 +91,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( sandbox_policy: SandboxPolicy::new_read_only_policy(), model: next_model.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -146,7 +146,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -179,7 +179,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul sandbox_policy: SandboxPolicy::new_read_only_policy(), model: next_model.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -234,8 +234,10 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -293,7 +295,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< sandbox_policy: SandboxPolicy::new_read_only_policy(), model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -312,7 +314,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< sandbox_policy: SandboxPolicy::new_read_only_policy(), model: text_model_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -391,8 +393,10 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -469,7 +473,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< sandbox_policy: SandboxPolicy::new_read_only_policy(), model: large_model_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -524,7 +528,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< sandbox_policy: SandboxPolicy::new_read_only_policy(), model: smaller_model_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs deleted file mode 100644 index 026be98dab..0000000000 --- a/codex-rs/core/tests/suite/model_tools.rs +++ /dev/null @@ -1,149 +0,0 @@ -#![allow(clippy::unwrap_used)] - -use codex_core::features::Feature; -use codex_protocol::config_types::WebSearchMode; -use core_test_support::responses; -use core_test_support::responses::start_mock_server; -use core_test_support::skip_if_no_network; -use core_test_support::test_codex::test_codex; - -#[allow(clippy::expect_used)] -fn tool_identifiers(body: &serde_json::Value) -> Vec { - body["tools"] - .as_array() - .unwrap() - .iter() - .map(|tool| { - tool.get("name") - .and_then(|v| v.as_str()) - .or_else(|| tool.get("type").and_then(|v| v.as_str())) - .map(std::string::ToString::to_string) - .expect("tool should have either name or type") - }) - .collect() -} - -#[allow(clippy::expect_used)] -async fn collect_tool_identifiers_for_model(model: &str) -> Vec { - let server = start_mock_server().await; - let sse = responses::sse(vec![ - responses::ev_response_created(model), - responses::ev_completed(model), - ]); - let resp_mock = responses::mount_sse_once(&server, sse).await; - - let mut builder = test_codex() - .with_model(model) - // Keep tool expectations stable when the default web_search mode changes. - .with_config(|config| { - config - .web_search_mode - .set(WebSearchMode::Cached) - .expect("test web_search_mode should satisfy constraints"); - config.features.enable(Feature::CollaborationModes); - }); - let test = builder - .build(&server) - .await - .expect("create test Codex conversation"); - - test.submit_turn("hello tools").await.expect("submit turn"); - - let body = resp_mock.single_request().body_json(); - tool_identifiers(&body) -} - -fn expected_default_tools(shell_tool: &str, tail: &[&str]) -> Vec { - let mut tools = if cfg!(windows) { - vec![shell_tool.to_string()] - } else { - vec!["exec_command".to_string(), "write_stdin".to_string()] - }; - tools.extend(tail.iter().map(|tool| (*tool).to_string())); - tools -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn model_selects_expected_tools() { - skip_if_no_network!(); - use pretty_assertions::assert_eq; - - let gpt51_codex_max_tools = collect_tool_identifiers_for_model("gpt-5.1-codex-max").await; - assert_eq!( - gpt51_codex_max_tools, - expected_default_tools( - "shell_command", - &[ - "update_plan", - "request_user_input", - "apply_patch", - "web_search", - "view_image", - ], - ), - "gpt-5.1-codex-max should expose the apply_patch tool", - ); - - let gpt5_codex_tools = collect_tool_identifiers_for_model("gpt-5-codex").await; - assert_eq!( - gpt5_codex_tools, - expected_default_tools( - "shell_command", - &[ - "update_plan", - "request_user_input", - "apply_patch", - "web_search", - "view_image", - ], - ), - "gpt-5-codex should expose the apply_patch tool", - ); - - let gpt51_codex_tools = collect_tool_identifiers_for_model("gpt-5.1-codex").await; - assert_eq!( - gpt51_codex_tools, - expected_default_tools( - "shell_command", - &[ - "update_plan", - "request_user_input", - "apply_patch", - "web_search", - "view_image", - ], - ), - "gpt-5.1-codex should expose the apply_patch tool", - ); - - let gpt5_tools = collect_tool_identifiers_for_model("gpt-5").await; - assert_eq!( - gpt5_tools, - expected_default_tools( - "shell", - &[ - "update_plan", - "request_user_input", - "web_search", - "view_image", - ], - ), - "gpt-5 should expose the apply_patch tool", - ); - - let gpt51_tools = collect_tool_identifiers_for_model("gpt-5.1").await; - assert_eq!( - gpt51_tools, - expected_default_tools( - "shell_command", - &[ - "update_plan", - "request_user_input", - "apply_patch", - "web_search", - "view_image", - ], - ), - "gpt-5.1 should expose the apply_patch tool", - ); -} diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 3288eb5c05..14503b3f5b 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use anyhow::Result; use codex_core::config::types::Personality; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -26,6 +25,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; +use serde_json::json; const PRETURN_CONTEXT_DIFF_CWD: &str = "PRETURN_CONTEXT_DIFF_CWD"; @@ -53,6 +53,30 @@ fn agents_message_count(request: &ResponsesRequest) -> usize { .count() } +fn format_environment_context_subagents_snapshot(subagents: &[&str]) -> String { + let subagents_block = if subagents.is_empty() { + String::new() + } else { + let lines = subagents + .iter() + .map(|line| format!(" {line}")) + .collect::>() + .join("\n"); + format!("\n \n{lines}\n ") + }; + let items = vec![json!({ + "type": "message", + "role": "user", + "content": [{ + "type": "input_text", + "text": format!( + "\n /tmp/example\n bash{subagents_block}\n" + ), + }], + })]; + context_snapshot::format_response_items_snapshot(items.as_slice(), &context_snapshot_options()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { skip_if_no_network!(Ok(())); @@ -97,7 +121,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -119,7 +143,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: Some(Personality::Friendly), }) @@ -196,7 +220,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -218,7 +242,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -323,7 +347,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul sandbox_policy: SandboxPolicy::new_read_only_policy(), model: resumed.session_configured.model.clone(), effort: resumed.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: Some(Personality::Friendly), }) @@ -445,3 +469,23 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn snapshot_model_visible_layout_environment_context_includes_one_subagent() -> Result<()> { + insta::assert_snapshot!( + "model_visible_layout_environment_context_includes_one_subagent", + format_environment_context_subagents_snapshot(&["- agent-1: Atlas"]) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn snapshot_model_visible_layout_environment_context_includes_two_subagents() -> Result<()> { + insta::assert_snapshot!( + "model_visible_layout_environment_context_includes_two_subagents", + format_environment_context_subagents_snapshot(&["- agent-1: Atlas", "- agent-2: Juniper"]) + ); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 3422e397b8..9948eb4e21 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -99,7 +99,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: test.session_configured.model.clone(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -336,8 +336,10 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 7c40bec987..1cdc549012 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use anyhow::Result; use codex_core::CodexAuth; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -106,7 +105,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 0b4d485cfb..97ce2360ba 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -97,7 +97,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -143,7 +143,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -196,7 +196,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -255,7 +255,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -302,7 +302,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -336,7 +336,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -398,7 +398,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -432,7 +432,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -504,7 +504,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -538,7 +538,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -599,8 +599,10 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow }), }), supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -646,7 +648,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: Some(Personality::Friendly), }) @@ -706,8 +708,10 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - }), }), supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -756,7 +760,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -790,7 +794,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 38a8145d50..54ee11fba4 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -30,21 +30,46 @@ use pretty_assertions::assert_eq; use tempfile::TempDir; fn text_user_input(text: String) -> serde_json::Value { + text_user_input_parts(vec![text]) +} + +fn text_user_input_parts(texts: Vec) -> serde_json::Value { serde_json::json!({ "type": "message", "role": "user", - "content": [ { "type": "input_text", "text": text } ] + "content": texts + .into_iter() + .map(|text| serde_json::json!({ "type": "input_text", "text": text })) + .collect::>() }) } -fn default_env_context_str(cwd: &str, shell: &Shell) -> String { +fn assert_default_env_context(text: &str, cwd: &str, shell: &Shell) { let shell_name = shell.name(); - format!( - r#" - {cwd} - {shell_name} -"# - ) + assert!( + text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG), + "expected environment context fragment: {text}" + ); + assert!( + text.contains(&format!("{cwd}")), + "expected cwd in environment context: {text}" + ); + assert!( + text.contains(&format!("{shell_name}")), + "expected shell in environment context: {text}" + ); + assert!( + text.contains("") && text.contains(""), + "expected current_date in environment context: {text}" + ); + assert!( + text.contains("") && text.contains(""), + "expected timezone in environment context: {text}" + ); + assert!( + text.ends_with(""), + "expected closing environment_context tag: {text}" + ); } fn assert_tool_names(body: &serde_json::Value, expected_names: &[&str]) { @@ -297,8 +322,8 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests let input1 = body1["input"].as_array().expect("input array"); assert_eq!( input1.len(), - 4, - "expected permissions + cached prefix + env + user msg" + 3, + "expected permissions + cached contextual user prefix + user msg" ); let ui_text = input1[1]["content"][0]["text"] @@ -311,13 +336,16 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests let shell = default_user_shell(); let cwd_str = config.cwd.to_string_lossy(); - let expected_env_text = default_env_context_str(&cwd_str, &shell); + let env_text = input1[1]["content"][1]["text"] + .as_str() + .expect("environment context text"); + assert_default_env_context(env_text, &cwd_str, &shell); assert_eq!( - input1[2], - text_user_input(expected_env_text), - "expected environment context after UI message" + input1[1]["content"][1]["type"].as_str(), + Some("input_text"), + "expected environment context bundled after UI message in cached contextual message" ); - assert_eq!(input1[3], text_user_input("hello 1".to_string())); + assert_eq!(input1[2], text_user_input("hello 1".to_string())); let body2 = req2.single_request().body_json(); let input2 = body2["input"].as_array().expect("input array"); @@ -402,8 +430,10 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let body1 = req1.single_request().body_json(); - let body2 = req2.single_request().body_json(); + let request1 = req1.single_request(); + let request2 = req2.single_request(); + let body1 = request1.body_json(); + let body2 = request2.body_json(); // prompt_cache_key should remain constant across overrides assert_eq!( body1["prompt_cache_key"], body2["prompt_cache_key"], @@ -500,17 +530,32 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul let env_texts: Vec<&str> = input .iter() .filter_map(|msg| { - msg["content"] - .as_array() - .and_then(|content| content.first()) - .and_then(|item| item["text"].as_str()) + msg["content"].as_array().map(|content| { + content + .iter() + .filter_map(|item| item["text"].as_str()) + .collect::>() + }) }) + .flatten() .filter(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)) .collect(); assert!( !env_texts.is_empty(), "expected environment context to be emitted: {env_texts:?}" ); + assert!( + env_texts + .iter() + .any(|text| text.contains("") && text.contains("")), + "expected current_date in environment context: {env_texts:?}" + ); + assert!( + env_texts + .iter() + .any(|text| text.contains("") && text.contains("")), + "expected timezone in environment context: {env_texts:?}" + ); let env_count = input .iter() @@ -559,11 +604,14 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul let user_texts: Vec<&str> = input .iter() .filter_map(|msg| { - msg["content"] - .as_array() - .and_then(|content| content.first()) - .and_then(|item| item["text"].as_str()) + msg["content"].as_array().map(|content| { + content + .iter() + .filter_map(|item| item["text"].as_str()) + .collect::>() + }) }) + .flatten() .collect(); assert!( user_texts.contains(&"first message"), @@ -631,7 +679,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res sandbox_policy: new_policy.clone(), model: "o3".to_string(), effort: Some(ReasoningEffort::High), - summary: ReasoningSummary::Detailed, + summary: Some(ReasoningSummary::Detailed), collaboration_mode: None, final_output_json_schema: None, personality: None, @@ -639,8 +687,10 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let body1 = req1.single_request().body_json(); - let body2 = req2.single_request().body_json(); + let request1 = req1.single_request(); + let request2 = req2.single_request(); + let body1 = request1.body_json(); + let body2 = request2.body_json(); // prompt_cache_key should remain constant across per-turn overrides assert_eq!( @@ -655,43 +705,34 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res "role": "user", "content": [ { "type": "input_text", "text": "hello 2" } ] }); - let shell = default_user_shell(); - - let expected_env_text_2 = format!( - r#" - {} - {} -"#, - new_cwd.path().display(), - shell.name() - ); - let expected_env_msg_2 = serde_json::json!({ - "type": "message", - "role": "user", - "content": [ { "type": "input_text", "text": expected_env_text_2 } ] - }); let expected_permissions_msg = body1["input"][0].clone(); let body1_input = body1["input"].as_array().expect("input array"); - let expected_model_switch_msg = body2["input"][body1_input.len()].clone(); + let expected_settings_update_msg = body2["input"][body1_input.len()].clone(); + assert_ne!( + expected_settings_update_msg, expected_permissions_msg, + "expected updated permissions message after per-turn override" + ); assert_eq!( - expected_model_switch_msg["role"].as_str(), + expected_settings_update_msg["role"].as_str(), Some("developer") ); assert!( - expected_model_switch_msg["content"][0]["text"] - .as_str() - .is_some_and(|text| text.contains("")), - "expected model switch message after model override: {expected_model_switch_msg:?}" - ); - let expected_permissions_msg_2 = body2["input"][body1_input.len() + 2].clone(); - assert_ne!( - expected_permissions_msg_2, expected_permissions_msg, - "expected updated permissions message after per-turn override" + request2.has_message_with_input_texts("developer", |texts| { + texts.iter().any(|text| text.contains("")) + }), + "expected model switch section after model override: {expected_settings_update_msg:?}" ); + let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone(); + assert_eq!(expected_env_msg_2["role"].as_str(), Some("user")); + let env_text = expected_env_msg_2["content"][0]["text"] + .as_str() + .expect("environment context text"); + let shell = default_user_shell(); + let expected_cwd = new_cwd.path().display().to_string(); + assert_default_env_context(env_text, &expected_cwd, &shell); let mut expected_body2 = body1_input.to_vec(); - expected_body2.push(expected_model_switch_msg); + expected_body2.push(expected_settings_update_msg); expected_body2.push(expected_env_msg_2); - expected_body2.push(expected_permissions_msg_2); expected_body2.push(expected_user_message_2); assert_eq!(body2["input"], serde_json::Value::Array(expected_body2)); @@ -746,7 +787,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a sandbox_policy: default_sandbox_policy.clone(), model: default_model.clone(), effort: default_effort, - summary: default_summary, + summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: None, final_output_json_schema: None, personality: None, @@ -765,7 +806,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a sandbox_policy: default_sandbox_policy.clone(), model: default_model.clone(), effort: default_effort, - summary: default_summary, + summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: None, final_output_json_schema: None, personality: None, @@ -773,22 +814,34 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let body1 = req1.single_request().body_json(); - let body2 = req2.single_request().body_json(); + let request1 = req1.single_request(); + let request2 = req2.single_request(); + let body1 = request1.body_json(); + let body2 = request2.body_json(); let expected_permissions_msg = body1["input"][0].clone(); let expected_ui_msg = body1["input"][1].clone(); let shell = default_user_shell(); let default_cwd_lossy = default_cwd.to_string_lossy(); + let expected_env_text_1 = expected_ui_msg["content"][1]["text"] + .as_str() + .expect("cached environment context text") + .to_string(); + assert_default_env_context(&expected_env_text_1, &default_cwd_lossy, &shell); - let expected_env_msg_1 = text_user_input(default_env_context_str(&default_cwd_lossy, &shell)); + let expected_contextual_user_msg_1 = text_user_input_parts(vec![ + expected_ui_msg["content"][0]["text"] + .as_str() + .expect("cached user instructions text") + .to_string(), + expected_env_text_1, + ]); let expected_user_message_1 = text_user_input("hello 1".to_string()); let expected_input_1 = serde_json::Value::Array(vec![ expected_permissions_msg.clone(), - expected_ui_msg.clone(), - expected_env_msg_1.clone(), + expected_contextual_user_msg_1.clone(), expected_user_message_1.clone(), ]); assert_eq!(body1["input"], expected_input_1); @@ -796,8 +849,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a let expected_user_message_2 = text_user_input("hello 2".to_string()); let expected_input_2 = serde_json::Value::Array(vec![ expected_permissions_msg, - expected_ui_msg, - expected_env_msg_1, + expected_contextual_user_msg_1, expected_user_message_1, expected_user_message_2, ]); @@ -854,7 +906,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu sandbox_policy: default_sandbox_policy.clone(), model: default_model, effort: default_effort, - summary: default_summary, + summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: None, final_output_json_schema: None, personality: None, @@ -873,7 +925,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu sandbox_policy: SandboxPolicy::DangerFullAccess, model: "o3".to_string(), effort: Some(ReasoningEffort::High), - summary: ReasoningSummary::Detailed, + summary: Some(ReasoningSummary::Detailed), collaboration_mode: None, final_output_json_schema: None, personality: None, @@ -881,49 +933,57 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let body1 = req1.single_request().body_json(); - let body2 = req2.single_request().body_json(); + let request1 = req1.single_request(); + let request2 = req2.single_request(); + let body1 = request1.body_json(); + let body2 = request2.body_json(); let expected_permissions_msg = body1["input"][0].clone(); let expected_ui_msg = body1["input"][1].clone(); let shell = default_user_shell(); - let expected_env_text_1 = default_env_context_str(&default_cwd.to_string_lossy(), &shell); - let expected_env_msg_1 = text_user_input(expected_env_text_1); + let expected_env_text_1 = expected_ui_msg["content"][1]["text"] + .as_str() + .expect("cached environment context text") + .to_string(); + assert_default_env_context(&expected_env_text_1, &default_cwd.to_string_lossy(), &shell); + let expected_contextual_user_msg_1 = text_user_input_parts(vec![ + expected_ui_msg["content"][0]["text"] + .as_str() + .expect("cached user instructions text") + .to_string(), + expected_env_text_1, + ]); let expected_user_message_1 = text_user_input("hello 1".to_string()); let expected_input_1 = serde_json::Value::Array(vec![ expected_permissions_msg.clone(), - expected_ui_msg.clone(), - expected_env_msg_1.clone(), + expected_contextual_user_msg_1.clone(), expected_user_message_1.clone(), ]); assert_eq!(body1["input"], expected_input_1); let body1_input = body1["input"].as_array().expect("input array"); - let expected_model_switch_msg = body2["input"][body1_input.len()].clone(); + let expected_settings_update_msg = body2["input"][body1_input.len()].clone(); + assert_ne!( + expected_settings_update_msg, expected_permissions_msg, + "expected updated permissions message after policy change" + ); assert_eq!( - expected_model_switch_msg["role"].as_str(), + expected_settings_update_msg["role"].as_str(), Some("developer") ); assert!( - expected_model_switch_msg["content"][0]["text"] - .as_str() - .is_some_and(|text| text.contains("")), - "expected model switch message after model override: {expected_model_switch_msg:?}" - ); - let expected_permissions_msg_2 = body2["input"][body1_input.len() + 1].clone(); - assert_ne!( - expected_permissions_msg_2, expected_permissions_msg, - "expected updated permissions message after policy change" + request2.has_message_with_input_texts("developer", |texts| { + texts.iter().any(|text| text.contains("")) + }), + "expected model switch section after model override: {expected_settings_update_msg:?}" ); let expected_user_message_2 = text_user_input("hello 2".to_string()); let expected_input_2 = serde_json::Value::Array(vec![ expected_permissions_msg, - expected_ui_msg, - expected_env_msg_1, + expected_contextual_user_msg_1, expected_user_message_1, - expected_model_switch_msg, - expected_permissions_msg_2, + expected_settings_update_msg, expected_user_message_2, ]); assert_eq!(body2["input"], expected_input_2); diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 98848e911c..1f7504d41b 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -715,6 +715,7 @@ async fn inbound_realtime_text_ignores_user_role_and_still_forwards_audio() -> R async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_audio() -> Result<()> { skip_if_no_network!(Ok(())); + let start = std::time::Instant::now(); let (gate_completed_tx, gate_completed_rx) = oneshot::channel(); let first_chunks = vec![ @@ -806,18 +807,45 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au _ => None, }) .await; + eprintln!( + "[realtime test +{}ms] saw trigger text={:?}", + start.elapsed().as_millis(), + "delegate now" + ); - let audio_out = tokio::time::timeout( - Duration::from_millis(500), - wait_for_event_match(&test.codex, |msg| match msg { - EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent { - payload: RealtimeEvent::AudioOut(frame), - }) => Some(frame.clone()), - _ => None, - }), - ) - .await - .expect("timed out waiting for realtime audio after echoed user-role message"); + let mirrored_request = realtime_server.wait_for_request(0, 1).await; + let mirrored_request_body = mirrored_request.body_json(); + eprintln!( + "[realtime test +{}ms] saw mirrored request type={:?} role={:?} text={:?} data={:?}", + start.elapsed().as_millis(), + mirrored_request_body["type"].as_str(), + mirrored_request_body["item"]["role"].as_str(), + mirrored_request_body["item"]["content"][0]["text"].as_str(), + mirrored_request_body["item"]["content"][0]["data"].as_str(), + ); + assert_eq!( + mirrored_request_body["type"].as_str(), + Some("conversation.item.create") + ); + assert_eq!( + mirrored_request_body["item"]["content"][0]["text"].as_str(), + Some("assistant says hi") + ); + + let audio_out = wait_for_event_match(&test.codex, |msg| match msg { + EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent { + payload: RealtimeEvent::AudioOut(frame), + }) => Some(frame.clone()), + _ => None, + }) + .await; + eprintln!( + "[realtime test +{}ms] saw audio out data={} sample_rate={} num_channels={}", + start.elapsed().as_millis(), + audio_out.data, + audio_out.sample_rate, + audio_out.num_channels + ); assert_eq!(audio_out.data, "AQID"); let completion = completions @@ -828,6 +856,10 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au completion .await .expect("delegated turn request did not complete"); + eprintln!( + "[realtime test +{}ms] delegated completion resolved", + start.elapsed().as_millis() + ); wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 74a5b6d90c..c304cc25fa 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -139,6 +139,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( }, ]; remote_model.supports_reasoning_summaries = true; + remote_model.default_reasoning_summary = ReasoningSummary::Detailed; mount_models_once( &server, ModelsResponse { @@ -175,7 +176,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: requested_model.to_string(), effort: None, - summary: config.model_reasoning_summary, + summary: None, collaboration_mode: None, personality: None, }) @@ -189,8 +190,13 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( .get("reasoning") .and_then(|reasoning| reasoning.get("effort")) .and_then(|value| value.as_str()); + let reasoning_summary = body + .get("reasoning") + .and_then(|reasoning| reasoning.get("summary")) + .and_then(|value| value.as_str()); assert_eq!(body["model"].as_str(), Some(requested_model)); assert_eq!(reasoning_effort, Some("high")); + assert_eq!(reasoning_summary, Some("detailed")); Ok(()) } @@ -227,7 +233,11 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: requested_model.to_string(), effort: None, - summary: config.model_reasoning_summary, + summary: Some( + config + .model_reasoning_summary + .unwrap_or(ReasoningSummary::Auto), + ), collaboration_mode: None, personality: None, }) @@ -284,8 +294,10 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -379,7 +391,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: REMOTE_MODEL_SLUG.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: Some(ReasoningSummary::Auto), collaboration_mode: None, personality: None, }) @@ -520,8 +532,10 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { base_instructions: remote_base.to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -590,7 +604,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: model.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: Some(ReasoningSummary::Auto), collaboration_mode: None, personality: None, }) @@ -830,8 +844,9 @@ async fn remote_models_request_times_out_after_5s() -> Result<()> { let elapsed = start.elapsed(); // get_model should return a default model even when refresh times out let default_model = model.expect("get_model should finish and return default model"); + let expected_default = bundled_default_model_slug(); assert!( - default_model == "gpt-5.2-codex", + default_model == expected_default, "get_model should return default model when refresh times out, got: {default_model}" ); let _ = server @@ -889,7 +904,7 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { let selected = manager .get_default_model(&None, RefreshStrategy::OnlineIfUncached) .await; - assert_eq!(selected, "gpt-5.2-codex"); + assert_eq!(selected, bundled_default_model_slug()); let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let hidden = available @@ -935,6 +950,15 @@ fn bundled_model_slug() -> String { .clone() } +fn bundled_default_model_slug() -> String { + codex_core::test_support::all_model_presets() + .iter() + .find(|preset| preset.is_default) + .expect("bundled models should include a default") + .model + .clone() +} + fn test_remote_model(slug: &str, visibility: ModelVisibility, priority: i32) -> ModelInfo { test_remote_model_with_policy( slug, @@ -970,8 +994,10 @@ fn test_remote_model_with_policy( base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy, supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index d6003e4b85..a6381de2d2 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -4,8 +4,8 @@ use anyhow::Result; use codex_core::config::Constrained; use codex_core::features::Feature; use codex_core::sandboxing::SandboxPermissions; -use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; @@ -13,6 +13,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; @@ -31,6 +32,11 @@ use regex_lite::Regex; use serde_json::Value; use serde_json::json; use std::fs; +use std::path::Path; + +fn absolute_path(path: &Path) -> AbsolutePathBuf { + AbsolutePathBuf::try_from(path).expect("absolute path") +} struct CommandResult { exit_code: Option, @@ -79,7 +85,7 @@ fn parse_result(item: &Value) -> CommandResult { fn shell_event_with_request_permissions( call_id: &str, command: &str, - additional_permissions: &AdditionalPermissions, + additional_permissions: &PermissionProfile, ) -> Result { let args = json!({ "command": command, @@ -91,6 +97,24 @@ fn shell_event_with_request_permissions( Ok(ev_function_call(call_id, "shell_command", &args_str)) } +#[cfg(target_os = "macos")] +fn shell_event_with_raw_request_permissions( + call_id: &str, + command: &str, + workdir: Option<&str>, + additional_permissions: Value, +) -> Result { + let args = json!({ + "command": command, + "workdir": workdir, + "timeout_ms": 1_000_u64, + "sandbox_permissions": SandboxPermissions::WithAdditionalPermissions, + "additional_permissions": additional_permissions, + }); + let args_str = serde_json::to_string(&args)?; + Ok(ev_function_call(call_id, "shell_command", &args_str)) +} + async fn submit_turn( test: &TestCodex, prompt: &str, @@ -110,7 +134,7 @@ async fn submit_turn( sandbox_policy, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -184,9 +208,12 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res let _ = fs::remove_file(&requested_write); let call_id = "request_permissions_skip_approval"; let command = "touch requested-but-unused.txt"; - let requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![requested_write.clone()], + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![absolute_path(&requested_write)]), + }), + ..Default::default() }; let event = shell_event_with_request_permissions(call_id, command, &requested_permissions)?; @@ -238,6 +265,98 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res Ok(()) } +#[tokio::test(flavor = "current_thread")] +#[cfg(target_os = "macos")] +async fn relative_additional_permissions_resolve_against_tool_workdir() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = start_mock_server().await; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let sandbox_policy_for_config = sandbox_policy.clone(); + + let mut builder = test_codex().with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config.features.enable(Feature::RequestPermissions); + }); + let test = builder.build(&server).await?; + + let nested_dir = test.workspace_path("nested"); + fs::create_dir_all(&nested_dir)?; + let requested_write = nested_dir.join("relative-write.txt"); + let _ = fs::remove_file(&requested_write); + + let call_id = "request_permissions_relative_workdir"; + let command = "touch relative-write.txt"; + let expected_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![absolute_path(&requested_write)]), + }), + ..Default::default() + }; + let event = shell_event_with_raw_request_permissions( + call_id, + command, + Some("nested"), + json!({ + "file_system": { + "write": ["./relative-write.txt"], + }, + }), + )?; + + let _ = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-relative-1"), + event, + ev_completed("resp-relative-1"), + ]), + ) + .await; + let results = mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-relative-1", "done"), + ev_completed("resp-relative-2"), + ]), + ) + .await; + + submit_turn(&test, call_id, approval_policy, sandbox_policy.clone()).await?; + + let approval = expect_exec_approval(&test, command).await; + assert_eq!( + approval.additional_permissions, + Some(expected_permissions.clone()) + ); + test.codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::Approved, + }) + .await?; + wait_for_completion(&test).await; + + let result = parse_result(&results.single_request().function_call_output(call_id)); + assert!( + result.exit_code.is_none() || result.exit_code == Some(0), + "unexpected exit code/output: {:?} {}", + result.exit_code, + result.stdout + ); + assert!( + requested_write.exists(), + "touch command should create requested path" + ); + + Ok(()) +} + #[tokio::test(flavor = "current_thread")] #[cfg(target_os = "macos")] async fn read_only_with_additional_permissions_widens_to_unrequested_cwd_write() -> Result<()> { @@ -266,9 +385,12 @@ async fn read_only_with_additional_permissions_widens_to_unrequested_cwd_write() "printf {:?} > {:?} && cat {:?}", "cwd-widened", unrequested_write, unrequested_write ); - let requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![requested_write.clone()], + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![absolute_path(&requested_write)]), + }), + ..Default::default() }; let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?; @@ -354,9 +476,12 @@ async fn read_only_with_additional_permissions_widens_to_unrequested_tmp_write() "printf {:?} > {:?} && cat {:?}", "tmp-widened", tmp_write, tmp_write ); - let requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![requested_write.clone()], + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![absolute_path(&requested_write)]), + }), + ..Default::default() }; let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?; @@ -442,13 +567,21 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() -> "printf {:?} > {:?} && cat {:?}", "outside-cwd-ok", outside_write, outside_write ); - let requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![outside_dir.path().to_path_buf()], + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![absolute_path(outside_dir.path())]), + }), + ..Default::default() }; - let normalized_requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![outside_dir.path().canonicalize()?], + let normalized_requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![AbsolutePathBuf::try_from( + outside_dir.path().canonicalize()?, + )?]), + }), + ..Default::default() }; let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?; @@ -530,13 +663,21 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul "printf {:?} > {:?} && cat {:?}", "should-not-write", outside_write, outside_write ); - let requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![outside_dir.path().to_path_buf()], + let requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![absolute_path(outside_dir.path())]), + }), + ..Default::default() }; - let normalized_requested_permissions = AdditionalPermissions { - fs_read: vec![], - fs_write: vec![outside_dir.path().canonicalize()?], + let normalized_requested_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![AbsolutePathBuf::try_from( + outside_dir.path().canonicalize()?, + )?]), + }), + ..Default::default() }; let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?; diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index ac6c1d2f3f..1f20f5dfdf 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -5,7 +5,6 @@ use std::collections::HashMap; use codex_core::features::Feature; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -86,8 +85,10 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul session_configured, .. } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); + .with_config(move |config| { + if mode == ModeKind::Default { + config.features.enable(Feature::DefaultModeRequestUserInput); + } }) .build(&server) .await?; @@ -136,7 +137,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: Some(CollaborationMode { mode, settings: Settings { @@ -198,18 +199,13 @@ where let server = start_mock_server().await; - let builder = test_codex(); + let mut builder = test_codex(); let TestCodex { codex, cwd, session_configured, .. - } = builder - .with_config(|config| { - config.features.enable(Feature::CollaborationModes); - }) - .build(&server) - .await?; + } = builder.build(&server).await?; let mode_slug = mode_name.to_lowercase().replace(' ', "-"); let call_id = format!("user-input-{mode_slug}-call"); @@ -257,7 +253,7 @@ where sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: Some(collaboration_mode), personality: None, }) @@ -290,7 +286,7 @@ async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<( } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn request_user_input_rejected_in_default_mode() -> anyhow::Result<()> { +async fn request_user_input_rejected_in_default_mode_by_default() -> anyhow::Result<()> { assert_request_user_input_rejected("Default", |model| CollaborationMode { mode: ModeKind::Default, settings: Settings { @@ -302,6 +298,11 @@ async fn request_user_input_rejected_in_default_mode() -> anyhow::Result<()> { .await } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn request_user_input_round_trip_in_default_mode_with_feature() -> anyhow::Result<()> { + request_user_input_round_trip_for_mode(ModeKind::Default).await +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn request_user_input_rejected_in_pair_mode_alias() -> anyhow::Result<()> { assert_request_user_input_rejected("Pair Programming", |model| CollaborationMode { diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index b0503d5929..4742b417ae 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -4,6 +4,7 @@ use codex_core::CodexAuth; use codex_core::NewThread; use codex_protocol::ThreadId; use codex_protocol::config_types::ModeKind; +use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::ResumedHistory; @@ -27,6 +28,8 @@ fn resume_history( let turn_ctx = TurnContextItem { turn_id: Some(turn_id.clone()), cwd: config.cwd.clone(), + current_date: None, + timezone: None, approval_policy: config.permissions.approval_policy.value(), sandbox_policy: config.permissions.sandbox_policy.get().clone(), network: None, @@ -34,7 +37,9 @@ fn resume_history( personality: None, collaboration_mode: None, effort: config.model_reasoning_effort, - summary: config.model_reasoning_summary, + summary: config + .model_reasoning_summary + .unwrap_or(ReasoningSummary::Auto), user_instructions: None, developer_instructions: None, final_output_json_schema: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index a123981444..d4b8ba80a4 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -605,7 +605,9 @@ async fn review_input_isolated_from_parent_history() { let env_text = input .iter() - .filter_map(|msg| msg["content"][0]["text"].as_str()) + .filter_map(|msg| msg.get("content").and_then(|content| content.as_array())) + .flat_map(|content| content.iter()) + .filter_map(|entry| entry.get("text").and_then(|text| text.as_str())) .find(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)) .expect("env text"); assert!( @@ -615,7 +617,9 @@ async fn review_input_isolated_from_parent_history() { let review_text = input .iter() - .filter_map(|msg| msg["content"][0]["text"].as_str()) + .filter_map(|msg| msg.get("content").and_then(|content| content.as_array())) + .flat_map(|content| content.iter()) + .filter_map(|entry| entry.get("text").and_then(|text| text.as_str())) .find(|text| *text == review_prompt) .expect("review prompt text"); assert_eq!( diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index df389c5ef7..dfbac85a09 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -104,6 +104,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -128,7 +129,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -246,6 +247,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -295,7 +297,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -398,8 +400,10 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -462,6 +466,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -492,7 +497,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re sandbox_policy: SandboxPolicy::new_read_only_policy(), model: text_only_model_slug.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -580,6 +585,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -604,7 +610,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -739,6 +745,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -763,7 +770,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -958,6 +965,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -982,7 +990,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index 247cfdaedc..263e4b96dc 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; @@ -49,7 +48,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> sandbox_policy: SandboxPolicy::DangerFullAccess, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -146,7 +145,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request sandbox_policy: SandboxPolicy::DangerFullAccess, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -230,7 +229,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul sandbox_policy: SandboxPolicy::DangerFullAccess, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -278,7 +277,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: REQUESTED_MODEL.to_string(), effort: test.config.model_reasoning_effort, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 1caab45b12..d852c9cfb3 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -133,6 +133,7 @@ fn rmcp_server_config(command: String) -> McpServerConfig { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, } } diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 3be9d2b579..a6f8ada10f 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -1,6 +1,5 @@ use anyhow::Result; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; @@ -162,7 +161,7 @@ async fn run_snapshot_command_with_options( sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -248,7 +247,7 @@ async fn run_shell_command_snapshot_with_options( sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -317,7 +316,7 @@ async fn run_tool_turn_on_harness( sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -535,7 +534,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index de0b57886b..846152a9e1 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -1,17 +1,20 @@ -#![allow(clippy::expect_used, clippy::unwrap_used)] +#![allow(clippy::unwrap_used)] +#![cfg(unix)] use anyhow::Result; +use codex_core::config::Config; use codex_core::features::Feature; +use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::Op; -use codex_protocol::skill_approval::SkillApprovalResponse; -use core_test_support::responses::ev_assistant_message; -use core_test_support::responses::ev_completed; -use core_test_support::responses::ev_function_call; -use core_test_support::responses::ev_response_created; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::user_input::UserInput; +use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::mount_function_call_agent_response; -use core_test_support::responses::mount_sse_sequence; -use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; @@ -24,17 +27,18 @@ use std::fs; use std::path::Path; use std::path::PathBuf; -fn write_skill_with_script(home: &Path, name: &str, script_body: &str) -> Result { - let skill_dir = home.join("skills").join(name); - let scripts_dir = skill_dir.join("scripts"); - fs::create_dir_all(&scripts_dir)?; - fs::write( - skill_dir.join("SKILL.md"), - format!("---\nname: {name}\ndescription: {name} skill\n---\n"), - )?; - let script_path = scripts_dir.join("run.py"); - fs::write(&script_path, script_body)?; - Ok(script_path) +fn absolute_path(path: &Path) -> AbsolutePathBuf { + match AbsolutePathBuf::try_from(path) { + Ok(path) => path, + Err(err) => panic!("absolute path: {err}"), + } +} + +fn write_skill_metadata(home: &Path, name: &str, contents: &str) -> Result<()> { + let metadata_dir = home.join("skills").join(name).join("agents"); + fs::create_dir_all(&metadata_dir)?; + fs::write(metadata_dir.join("openai.yaml"), contents)?; + Ok(()) } fn shell_command_arguments(command: &str) -> Result { @@ -44,35 +48,25 @@ fn shell_command_arguments(command: &str) -> Result { }))?) } -fn assistant_response(message: &str) -> String { - sse(vec![ - ev_response_created("resp-2"), - ev_assistant_message("msg-1", message), - ev_completed("resp-2"), - ]) -} - -fn command_for_script(script_path: &Path) -> Result { - let runner = if cfg!(windows) { "python" } else { "python3" }; - let script_path = script_path.to_string_lossy().into_owned(); - Ok(shlex::try_join([runner, script_path.as_str()])?) -} - -async fn submit_turn(test: &TestCodex, prompt: &str) -> Result<()> { - let session_model = test.session_configured.model.clone(); +async fn submit_turn_with_policies( + test: &TestCodex, + prompt: &str, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, +) -> Result<()> { test.codex .submit(Op::UserTurn { - items: vec![codex_protocol::user_input::UserInput::Text { + items: vec![UserInput::Text { text: prompt.to_string(), text_elements: Vec::new(), }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), - approval_policy: codex_protocol::protocol::AskForApproval::Never, - sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - model: session_model, + approval_policy, + sandbox_policy, + model: test.session_configured.model.clone(), effort: None, - summary: codex_protocol::config_types::ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -80,240 +74,680 @@ async fn submit_turn(test: &TestCodex, prompt: &str) -> Result<()> { Ok(()) } -async fn wait_for_turn_complete_without_skill_approval(test: &TestCodex) { - wait_for_event_match(test.codex.as_ref(), |event| match event { - EventMsg::SkillRequestApproval(request) => { - panic!("unexpected skill approval request: {request:?}"); - } - EventMsg::TurnComplete(_) => Some(()), - _ => None, - }) - .await; +fn write_skill_with_shell_script(home: &Path, name: &str, script_name: &str) -> Result { + write_skill_with_shell_script_contents( + home, + name, + script_name, + r#"#!/bin/sh +echo 'zsh-fork-stdout' +echo 'zsh-fork-stderr' >&2 +"#, + ) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn skill_approval_event_round_trip_for_shell_command_skill_script_exec() -> Result<()> { - skip_if_no_network!(Ok(())); +#[cfg(unix)] +fn write_skill_with_shell_script_contents( + home: &Path, + name: &str, + script_name: &str, + script_contents: &str, +) -> Result { + use std::os::unix::fs::PermissionsExt; - let server = start_mock_server().await; - let tool_call_id = "shell-skill-call"; + let skill_dir = home.join("skills").join(name); + let scripts_dir = skill_dir.join("scripts"); + fs::create_dir_all(&scripts_dir)?; + fs::write( + skill_dir.join("SKILL.md"), + format!( + r#"--- +name: {name} +description: {name} skill +--- +"# + ), + )?; + + let script_path = scripts_dir.join(script_name); + fs::write(&script_path, script_contents)?; + let mut permissions = fs::metadata(&script_path)?.permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script_path, permissions)?; + Ok(script_path) +} + +fn find_test_zsh_path() -> Result> { + use core_test_support::fetch_dotslash_file; + + let repo_root = codex_utils_cargo_bin::repo_root()?; + let dotslash_zsh = repo_root.join("codex-rs/app-server/tests/suite/zsh"); + if !dotslash_zsh.is_file() { + eprintln!( + "skipping zsh-fork skill test: shared zsh DotSlash file not found at {}", + dotslash_zsh.display() + ); + return Ok(None); + } + + match fetch_dotslash_file(&dotslash_zsh, None) { + Ok(path) => Ok(Some(path)), + Err(error) => { + eprintln!("skipping zsh-fork skill test: failed to fetch zsh via dotslash: {error:#}"); + Ok(None) + } + } +} + +fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool { + let status = std::process::Command::new(zsh_path) + .arg("-fc") + .arg("/usr/bin/true") + .env("EXEC_WRAPPER", "/usr/bin/false") + .status(); + match status { + Ok(status) => !status.success(), + Err(_) => false, + } +} + +#[derive(Clone)] +struct ZshForkRuntime { + zsh_path: PathBuf, + main_execve_wrapper_exe: PathBuf, +} + +impl ZshForkRuntime { + fn apply_to_config( + &self, + config: &mut Config, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + ) { + use codex_config::Constrained; + + config.features.enable(Feature::ShellTool); + config.features.enable(Feature::ShellZshFork); + config.zsh_path = Some(self.zsh_path.clone()); + config.main_execve_wrapper_exe = Some(self.main_execve_wrapper_exe.clone()); + config.permissions.allow_login_shell = false; + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy); + } +} + +fn restrictive_workspace_write_policy() -> SandboxPolicy { + SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + read_only_access: Default::default(), + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } +} + +fn zsh_fork_runtime(test_name: &str) -> Result> { + let Some(zsh_path) = find_test_zsh_path()? else { + return Ok(None); + }; + if !supports_exec_wrapper_intercept(&zsh_path) { + eprintln!( + "skipping {test_name}: zsh does not support EXEC_WRAPPER intercepts ({})", + zsh_path.display() + ); + return Ok(None); + } + let Ok(main_execve_wrapper_exe) = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper") + else { + eprintln!("skipping {test_name}: unable to resolve `codex-execve-wrapper` binary"); + return Ok(None); + }; + + Ok(Some(ZshForkRuntime { + zsh_path, + main_execve_wrapper_exe, + })) +} + +async fn build_zsh_fork_test( + server: &wiremock::MockServer, + runtime: ZshForkRuntime, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + pre_build_hook: F, +) -> Result +where + F: FnOnce(&Path) + Send + 'static, +{ let mut builder = test_codex() - .with_pre_build_hook(|home| { - write_skill_with_script(home, "demo", "print('shell skill approved')").unwrap(); - }) - .with_config(|config| { - config.features.enable(Feature::SkillApproval); + .with_pre_build_hook(pre_build_hook) + .with_config(move |config| { + runtime.apply_to_config(config, approval_policy, sandbox_policy); }); - let test = builder.build(&server).await?; - let script_path = test.codex_home_path().join("skills/demo/scripts/run.py"); - let command = command_for_script(&script_path)?; - let arguments = shell_command_arguments(&command)?; - let _mocks = - mount_function_call_agent_response(&server, tool_call_id, &arguments, "shell_command") - .await; + builder.build(server).await +} - submit_turn(&test, "run the shell skill").await?; +fn skill_script_command(test: &TestCodex, script_name: &str) -> Result<(String, String)> { + let script_path = fs::canonicalize( + test.codex_home_path() + .join("skills/mbolin-test-skill/scripts") + .join(script_name), + )?; + let script_path_str = script_path.to_string_lossy().into_owned(); + let command = shlex::try_join([script_path_str.as_str()])?; + Ok((script_path_str, command)) +} - let request = wait_for_event_match(test.codex.as_ref(), |event| match event { - EventMsg::SkillRequestApproval(request) => Some(request.clone()), +async fn wait_for_exec_approval_request(test: &TestCodex) -> Option { + wait_for_event_match(test.codex.as_ref(), |event| match event { + EventMsg::ExecApprovalRequest(request) => Some(Some(request.clone())), + EventMsg::TurnComplete(_) => Some(None), _ => None, }) - .await; - assert_eq!(request.item_id, tool_call_id); - assert_eq!(request.skill_name, "demo"); - - test.codex - .submit(Op::SkillApproval { - id: request.item_id, - response: SkillApprovalResponse { approved: true }, - }) - .await?; + .await +} +async fn wait_for_turn_complete(test: &TestCodex) { wait_for_event(test.codex.as_ref(), |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; - - Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn skill_approval_not_emitted_without_skill_script_exec() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - let tool_call_id = "non-skill-call"; - let mut builder = test_codex().with_config(|config| { - config.features.enable(Feature::SkillApproval); - }); - let test = builder.build(&server).await?; - let arguments = shell_command_arguments("echo no-skill")?; - let _mocks = - mount_function_call_agent_response(&server, tool_call_id, &arguments, "shell_command") - .await; - - submit_turn(&test, "run a plain command").await?; - wait_for_turn_complete_without_skill_approval(&test).await; - - Ok(()) +fn output_shows_sandbox_denial(output: &str) -> bool { + output.contains("Permission denied") + || output.contains("Operation not permitted") + || output.contains("Read-only file system") } +/// Focus on the approval payload: the skill should prompt before execution and +/// only advertise the permissions declared in its metadata. +#[cfg(unix)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn skill_approval_decline_blocks_execution() -> Result<()> { +async fn shell_zsh_fork_prompts_for_skill_script_execution() -> Result<()> { skip_if_no_network!(Ok(())); + let Some(runtime) = zsh_fork_runtime("zsh-fork skill prompt test")? else { + return Ok(()); + }; + let server = start_mock_server().await; - let tool_call_id = "decline-call"; - let marker_name = "declined-marker.txt"; - let mut builder = test_codex() - .with_pre_build_hook(move |home| { - let marker_path = home.join(marker_name); - let marker_path = marker_path.to_string_lossy(); - let script_body = format!( - "from pathlib import Path\nPath({marker_path:?}).write_text('ran')\nprint('ran')\n" - ); - write_skill_with_script(home, "demo", &script_body).unwrap(); - }) - .with_config(|config| { - config.features.enable(Feature::SkillApproval); - }); - let test = builder.build(&server).await?; - let script_path = test.codex_home_path().join("skills/demo/scripts/run.py"); - let command = command_for_script(&script_path)?; + let tool_call_id = "zsh-fork-skill-call"; + let test = build_zsh_fork_test( + &server, + runtime, + AskForApproval::OnRequest, + SandboxPolicy::new_workspace_write_policy(), + |home| { + write_skill_with_shell_script(home, "mbolin-test-skill", "hello-mbolin.sh").unwrap(); + write_skill_metadata( + home, + "mbolin-test-skill", + r#" +permissions: + file_system: + read: + - "./data" + write: + - "./output" +"#, + ) + .unwrap(); + }, + ) + .await?; + + let (script_path_str, command) = skill_script_command(&test, "hello-mbolin.sh")?; let arguments = shell_command_arguments(&command)?; let mocks = mount_function_call_agent_response(&server, tool_call_id, &arguments, "shell_command") .await; - submit_turn(&test, "run the skill").await?; + submit_turn_with_policies( + &test, + "use $mbolin-test-skill", + AskForApproval::OnRequest, + SandboxPolicy::new_workspace_write_policy(), + ) + .await?; - let request = wait_for_event_match(test.codex.as_ref(), |event| match event { - EventMsg::SkillRequestApproval(request) => Some(request.clone()), - _ => None, - }) - .await; - assert_eq!(request.item_id, tool_call_id); + let maybe_approval = wait_for_exec_approval_request(&test).await; + let approval = match maybe_approval { + Some(approval) => approval, + None => { + let call_output = mocks + .completion + .single_request() + .function_call_output(tool_call_id); + panic!( + "expected exec approval request before completion; function_call_output={call_output:?}" + ); + } + }; + assert_eq!(approval.call_id, tool_call_id); + assert_eq!(approval.command, vec![script_path_str.clone()]); + assert_eq!( + approval.available_decisions, + Some(vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::Abort, + ]) + ); + assert_eq!( + approval.additional_permissions, + Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![absolute_path( + &test.codex_home_path().join("skills/mbolin-test-skill/data"), + )]), + write: Some(vec![absolute_path( + &test + .codex_home_path() + .join("skills/mbolin-test-skill/output"), + )]), + }), + ..Default::default() + }) + ); test.codex - .submit(Op::SkillApproval { - id: request.item_id, - response: SkillApprovalResponse { approved: false }, + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::Denied, }) .await?; - wait_for_event(test.codex.as_ref(), |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; - - let marker_path = test.codex_home_path().join(marker_name); - assert!( - !marker_path.exists(), - "declined skill approval should block script execution" - ); + wait_for_turn_complete(&test).await; let call_output = mocks .completion .single_request() .function_call_output(tool_call_id); - assert_eq!( - call_output["output"].as_str(), - Some("This script is part of the skill and the user declined the skill usage"), + let output = call_output["output"].as_str().unwrap_or_default(); + assert!( + output.contains("Execution denied: User denied execution"), + "expected rejection marker in function_call_output: {output:?}" ); Ok(()) } +/// Look for `additional_permissions == None`, then verify that both the first +/// run and the cached session-approval rerun stay inside the turn sandbox. +#[cfg(unix)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn skill_approval_cache_is_per_skill() -> Result<()> { +async fn shell_zsh_fork_skill_without_permissions_inherits_turn_sandbox() -> Result<()> { skip_if_no_network!(Ok(())); - let server = start_mock_server().await; - let first_call_id = "skill-a-1"; - let second_call_id = "skill-a-2"; - let third_call_id = "skill-b-1"; - let mut builder = test_codex() - .with_pre_build_hook(|home| { - write_skill_with_script(home, "alpha", "print('alpha')").unwrap(); - write_skill_with_script(home, "beta", "print('beta')").unwrap(); - }) - .with_config(|config| { - config.features.enable(Feature::SkillApproval); - }); - let test = builder.build(&server).await?; - let alpha_command = - command_for_script(&test.codex_home_path().join("skills/alpha/scripts/run.py"))?; - let beta_command = - command_for_script(&test.codex_home_path().join("skills/beta/scripts/run.py"))?; - let first_alpha_arguments = shell_command_arguments(&alpha_command)?; - let second_alpha_arguments = shell_command_arguments(&alpha_command)?; - let beta_arguments = shell_command_arguments(&beta_command)?; + let Some(runtime) = zsh_fork_runtime("zsh-fork inherited skill sandbox test")? else { + return Ok(()); + }; - mount_sse_sequence( + let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?; + let outside_path = outside_dir + .path() + .join("zsh-fork-skill-inherited-sandbox.txt"); + let outside_path_quoted = shlex::try_join([outside_path.to_string_lossy().as_ref()])?; + let script_contents = format!( + "#!/bin/sh\nprintf '%s' forbidden > {outside_path_quoted}\ncat {outside_path_quoted}\n" + ); + let outside_path_for_hook = outside_path.clone(); + let script_contents_for_hook = script_contents.clone(); + let workspace_write_policy = restrictive_workspace_write_policy(); + + let server = start_mock_server().await; + let test = build_zsh_fork_test( &server, - vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_function_call(first_call_id, "shell_command", &first_alpha_arguments), - ev_completed("resp-1"), - ]), - assistant_response("alpha-1"), - sse(vec![ - ev_response_created("resp-1"), - ev_function_call(second_call_id, "shell_command", &second_alpha_arguments), - ev_completed("resp-1"), - ]), - assistant_response("alpha-2"), - sse(vec![ - ev_response_created("resp-1"), - ev_function_call(third_call_id, "shell_command", &beta_arguments), - ev_completed("resp-1"), - ]), - assistant_response("beta-1"), - ], + runtime, + AskForApproval::OnRequest, + workspace_write_policy.clone(), + move |home| { + let _ = fs::remove_file(&outside_path_for_hook); + write_skill_with_shell_script_contents( + home, + "mbolin-test-skill", + "sandboxed.sh", + &script_contents_for_hook, + ) + .unwrap(); + }, + ) + .await?; + + let (script_path_str, command) = skill_script_command(&test, "sandboxed.sh")?; + + let first_call_id = "zsh-fork-skill-permissions-1"; + let first_arguments = shell_command_arguments(&command)?; + let first_mocks = mount_function_call_agent_response( + &server, + first_call_id, + &first_arguments, + "shell_command", ) .await; - submit_turn(&test, "run alpha").await?; - let first_request = wait_for_event_match(test.codex.as_ref(), |event| match event { - EventMsg::SkillRequestApproval(request) => Some(request.clone()), - _ => None, - }) - .await; - assert_eq!(first_request.item_id, first_call_id); - assert_eq!(first_request.skill_name, "alpha"); + submit_turn_with_policies( + &test, + "use $mbolin-test-skill", + AskForApproval::OnRequest, + workspace_write_policy.clone(), + ) + .await?; + + let maybe_approval = wait_for_exec_approval_request(&test).await; + let approval = match maybe_approval { + Some(approval) => approval, + None => panic!("expected exec approval request before completion"), + }; + assert_eq!(approval.call_id, first_call_id); + assert_eq!(approval.command, vec![script_path_str.clone()]); + assert_eq!(approval.additional_permissions, None); + test.codex - .submit(Op::SkillApproval { - id: first_request.item_id, - response: SkillApprovalResponse { approved: true }, + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::ApprovedForSession, }) .await?; - wait_for_event(test.codex.as_ref(), |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) + + wait_for_turn_complete(&test).await; + + let first_output = first_mocks + .completion + .single_request() + .function_call_output(first_call_id)["output"] + .as_str() + .unwrap_or_default() + .to_string(); + assert!( + output_shows_sandbox_denial(&first_output) || !first_output.contains("forbidden"), + "expected inherited turn sandbox denial on first run, got output: {first_output:?}" + ); + assert!( + !outside_path.exists(), + "first run should not write outside the turn sandbox" + ); + + let second_call_id = "zsh-fork-skill-permissions-2"; + let second_arguments = shell_command_arguments(&command)?; + let second_mocks = mount_function_call_agent_response( + &server, + second_call_id, + &second_arguments, + "shell_command", + ) .await; - submit_turn(&test, "run alpha again").await?; - wait_for_turn_complete_without_skill_approval(&test).await; + submit_turn_with_policies( + &test, + "use $mbolin-test-skill", + AskForApproval::OnRequest, + workspace_write_policy, + ) + .await?; - submit_turn(&test, "run beta").await?; - let third_request = wait_for_event_match(test.codex.as_ref(), |event| match event { - EventMsg::SkillRequestApproval(request) => Some(request.clone()), - _ => None, - }) - .await; - assert_eq!(third_request.item_id, third_call_id); - assert_eq!(third_request.skill_name, "beta"); - test.codex - .submit(Op::SkillApproval { - id: third_request.item_id, - response: SkillApprovalResponse { approved: true }, - }) - .await?; - wait_for_event(test.codex.as_ref(), |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; + let cached_approval = wait_for_exec_approval_request(&test).await; + assert!( + cached_approval.is_none(), + "expected second run to reuse the cached session approval" + ); + + let second_output = second_mocks + .completion + .single_request() + .function_call_output(second_call_id)["output"] + .as_str() + .unwrap_or_default() + .to_string(); + assert!( + output_shows_sandbox_denial(&second_output) || !second_output.contains("forbidden"), + "expected cached skill approval to retain inherited turn sandboxing, got output: {second_output:?}" + ); + assert!( + !outside_path.exists(), + "cached session approval should not widen a permissionless skill to full access" + ); + + Ok(()) +} + +/// The validation to focus on is: writes to the skill-approved folder succeed, +/// and writes to an unrelated folder fail, both before and after cached approval. +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn shell_zsh_fork_skill_session_approval_enforces_skill_permissions() -> Result<()> { + skip_if_no_network!(Ok(())); + + let Some(runtime) = zsh_fork_runtime("zsh-fork explicit skill sandbox test")? else { + return Ok(()); + }; + + let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?; + let allowed_dir = outside_dir.path().join("allowed-output"); + let blocked_dir = outside_dir.path().join("blocked-output"); + fs::create_dir_all(&allowed_dir)?; + fs::create_dir_all(&blocked_dir)?; + + let allowed_path = allowed_dir.join("allowed.txt"); + let blocked_path = blocked_dir.join("blocked.txt"); + let allowed_path_quoted = shlex::try_join([allowed_path.to_string_lossy().as_ref()])?; + let blocked_path_quoted = shlex::try_join([blocked_path.to_string_lossy().as_ref()])?; + let script_contents = format!( + "#!/bin/sh\nprintf '%s' allowed > {allowed_path_quoted}\ncat {allowed_path_quoted}\nprintf '%s' forbidden > {blocked_path_quoted}\nif [ -f {blocked_path_quoted} ]; then echo blocked-created; fi\n" + ); + let allowed_dir_for_hook = allowed_dir.clone(); + let allowed_path_for_hook = allowed_path.clone(); + let blocked_path_for_hook = blocked_path.clone(); + let script_contents_for_hook = script_contents.clone(); + + let permissions_yaml = format!( + "permissions:\n file_system:\n write:\n - \"{}\"\n", + allowed_dir.display() + ); + + let workspace_write_policy = restrictive_workspace_write_policy(); + let server = start_mock_server().await; + let test = build_zsh_fork_test( + &server, + runtime, + AskForApproval::OnRequest, + workspace_write_policy.clone(), + move |home| { + let _ = fs::remove_file(&allowed_path_for_hook); + let _ = fs::remove_file(&blocked_path_for_hook); + fs::create_dir_all(&allowed_dir_for_hook).unwrap(); + fs::create_dir_all(blocked_path_for_hook.parent().unwrap()).unwrap(); + write_skill_with_shell_script_contents( + home, + "mbolin-test-skill", + "sandboxed.sh", + &script_contents_for_hook, + ) + .unwrap(); + write_skill_metadata(home, "mbolin-test-skill", &permissions_yaml).unwrap(); + }, + ) + .await?; + + let (script_path_str, command) = skill_script_command(&test, "sandboxed.sh")?; + + let first_call_id = "zsh-fork-skill-permissions-1"; + let first_arguments = shell_command_arguments(&command)?; + let first_mocks = mount_function_call_agent_response( + &server, + first_call_id, + &first_arguments, + "shell_command", + ) + .await; + + submit_turn_with_policies( + &test, + "use $mbolin-test-skill", + AskForApproval::OnRequest, + workspace_write_policy.clone(), + ) + .await?; + + let maybe_approval = wait_for_exec_approval_request(&test).await; + let approval = match maybe_approval { + Some(approval) => approval, + None => panic!("expected exec approval request before completion"), + }; + assert_eq!(approval.call_id, first_call_id); + assert_eq!(approval.command, vec![script_path_str.clone()]); + assert_eq!( + approval.additional_permissions, + Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: None, + write: Some(vec![absolute_path(&allowed_dir)]), + }), + ..Default::default() + }) + ); + + test.codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::ApprovedForSession, + }) + .await?; + + wait_for_turn_complete(&test).await; + + let first_output = first_mocks + .completion + .single_request() + .function_call_output(first_call_id)["output"] + .as_str() + .unwrap_or_default() + .to_string(); + assert!( + first_output.contains("allowed"), + "expected skill sandbox to permit writes to the approved folder, got output: {first_output:?}" + ); + assert_eq!(fs::read_to_string(&allowed_path)?, "allowed"); + assert!( + !blocked_path.exists(), + "first run should not write outside the explicit skill sandbox" + ); + assert!( + !first_output.contains("blocked-created"), + "blocked path should not have been created: {first_output:?}" + ); + + let second_call_id = "zsh-fork-skill-permissions-2"; + let second_arguments = shell_command_arguments(&command)?; + let second_mocks = mount_function_call_agent_response( + &server, + second_call_id, + &second_arguments, + "shell_command", + ) + .await; + + let _ = fs::remove_file(&allowed_path); + let _ = fs::remove_file(&blocked_path); + + submit_turn_with_policies( + &test, + "use $mbolin-test-skill", + AskForApproval::OnRequest, + workspace_write_policy, + ) + .await?; + + let cached_approval = wait_for_exec_approval_request(&test).await; + assert!( + cached_approval.is_none(), + "expected second run to reuse the cached session approval" + ); + + let second_output = second_mocks + .completion + .single_request() + .function_call_output(second_call_id)["output"] + .as_str() + .unwrap_or_default() + .to_string(); + assert!( + second_output.contains("allowed"), + "expected cached skill approval to retain the explicit skill sandbox, got output: {second_output:?}" + ); + assert_eq!(fs::read_to_string(&allowed_path)?, "allowed"); + assert!( + !blocked_path.exists(), + "cached session approval should not widen skill execution beyond the explicit skill sandbox" + ); + assert!( + !second_output.contains("blocked-created"), + "blocked path should not have been created after cached approval: {second_output:?}" + ); + + Ok(()) +} + +/// This stays narrow on purpose: the important check is that `WorkspaceWrite` +/// continues to deny writes outside the workspace even under `zsh-fork`. +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> { + skip_if_no_network!(Ok(())); + + let Some(runtime) = zsh_fork_runtime("zsh-fork workspace sandbox test")? else { + return Ok(()); + }; + + let server = start_mock_server().await; + let tool_call_id = "zsh-fork-workspace-write-deny"; + let outside_path = "/tmp/codex-zsh-fork-workspace-write-deny.txt"; + let workspace_write_policy = restrictive_workspace_write_policy(); + let _ = fs::remove_file(outside_path); + let test = build_zsh_fork_test( + &server, + runtime, + AskForApproval::Never, + workspace_write_policy.clone(), + move |_| { + let _ = fs::remove_file(outside_path); + }, + ) + .await?; + + let command = format!("touch {outside_path}"); + let arguments = shell_command_arguments(&command)?; + let mocks = + mount_function_call_agent_response(&server, tool_call_id, &arguments, "shell_command") + .await; + + submit_turn_with_policies( + &test, + "write outside workspace with zsh fork", + AskForApproval::Never, + workspace_write_policy, + ) + .await?; + + wait_for_turn_complete(&test).await; + + let call_output = mocks + .completion + .single_request() + .function_call_output(tool_call_id); + let output = call_output["output"].as_str().unwrap_or_default(); + assert!( + output_shows_sandbox_denial(output), + "expected sandbox denial, got output: {output:?}" + ); + assert!( + !Path::new(outside_path).exists(), + "command should not write outside workspace under WorkspaceWrite policy" + ); Ok(()) } diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index f681ca1b64..2f786590b1 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -77,7 +77,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: codex_protocol::config_types::ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap index c88ff33122..e15d55aab2 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap @@ -6,16 +6,18 @@ Scenario: Manual /compact with prior user history compacts existing history and ## Local Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:first manual turn -04:message/assistant:FIRST_REPLY -05:message/user: +01:message/user[2]: + [01] + [02] > +02:message/user:first manual turn +03:message/assistant:FIRST_REPLY +04:message/user: ## Local Post-Compaction History Layout 00:message/user:first manual turn 01:message/user:\nFIRST_MANUAL_SUMMARY 02:message/developer: -03:message/user: -04:message/user:> -05:message/user:second manual turn +03:message/user[2]: + [01] + [02] > +04:message/user:second manual turn diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap index 0c8f87d04c..ca07006eae 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap @@ -6,13 +6,15 @@ Scenario: Manual /compact with no prior user turn currently still issues a compa ## Local Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user: +01:message/user[2]: + [01] + [02] > +02:message/user: ## Local Post-Compaction History Layout 00:message/user:\nMANUAL_EMPTY_SUMMARY 01:message/developer: -02:message/user: -03:message/user:> -04:message/user:AFTER_MANUAL_EMPTY_COMPACT +02:message/user[2]: + [01] + [02] > +03:message/user:AFTER_MANUAL_EMPTY_COMPACT diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap index c13f78aff9..f59fdf4b96 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap @@ -1,22 +1,23 @@ --- source: core/tests/suite/compact.rs -assertion_line: 2646 expression: "format_labeled_requests_snapshot(\"True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn.\",\n&[(\"Local Compaction Request\", &auto_compact_mock.single_request()),\n(\"Local Post-Compaction History Layout\",\n&post_auto_compact_mock.single_request()),])" --- Scenario: True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn. ## Local Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:function call limit push -04:function_call/test_tool -05:function_call_output:unsupported call: test_tool -06:message/user: +01:message/user[2]: + [01] + [02] > +02:message/user:function call limit push +03:function_call/test_tool +04:function_call_output:unsupported call: test_tool +05:message/user: ## Local Post-Compaction History Layout 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:function call limit push -04:message/user:\nAUTO_SUMMARY +01:message/user[2]: + [01] + [02] > +02:message/user:function call limit push +03:message/user:\nAUTO_SUMMARY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap index 552c117c98..7f61d7ed5e 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap @@ -1,30 +1,35 @@ --- source: core/tests/suite/compact.rs +assertion_line: 1791 expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Pre-sampling Compaction Request\", &requests[1]),\n(\"Post-Compaction Follow-up Request (Next Model)\", &requests[2]),])" --- Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message. ## Initial Request (Previous Model) 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:before switch +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:before switch ## Pre-sampling Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:before switch -05:message/assistant:before switch -06:message/user: +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:before switch +04:message/assistant:before switch +05:message/user: ## Post-Compaction Follow-up Request (Next Model) 00:message/user:before switch 01:message/user:\nPRE_SAMPLING_SUMMARY -02:message/developer:\nThe user was previously using a different model.... -03:message/developer: -04:message/user: -05:message/user:> -06:message/user:after switch +02:message/developer[2]: + [01] \nThe user was previously using a different model.... + [02] +03:message/user[2]: + [01] + [02] > +04:message/user:after switch diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap index da30b01bd7..0de8baeebe 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap @@ -6,8 +6,9 @@ Scenario: Pre-turn auto-compaction context-window failure: compaction request ex ## Local Compaction Request (Incoming User Excluded) 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:message/assistant:FIRST_REPLY -05:message/user: +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:message/assistant:FIRST_REPLY +04:message/user: diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index ea4fea2f21..8712df5833 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -6,19 +6,25 @@ Scenario: Pre-turn auto-compaction with a context override emits the context dif ## Local Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:message/assistant:FIRST_REPLY -05:message/user:USER_TWO -06:message/assistant:SECOND_REPLY -07:message/user: +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:message/assistant:FIRST_REPLY +04:message/user:USER_TWO +05:message/assistant:SECOND_REPLY +06:message/user: ## Local Post-Compaction History Layout 00:message/user:USER_ONE 01:message/user:USER_TWO 02:message/user:\nPRE_TURN_SUMMARY 03:message/developer: -04:message/user: -05:message/user: -06:message/user: | | | USER_THREE +04:message/user[2]: + [01] + [02] +05:message/user[4]: + [01] + [02] + [03] + [04] USER_THREE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap index 59d3785a43..46d76bb100 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap @@ -1,31 +1,36 @@ --- source: core/tests/suite/compact.rs +assertion_line: 3188 expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])" --- Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming from the compact request and restores it in the post-compaction follow-up request. ## Initial Request (Previous Model) 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:BEFORE_SWITCH_USER +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:BEFORE_SWITCH_USER ## Local Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:BEFORE_SWITCH_USER -05:message/assistant:BEFORE_SWITCH_REPLY -06:message/user: +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:BEFORE_SWITCH_USER +04:message/assistant:BEFORE_SWITCH_REPLY +05:message/user: ## Local Post-Compaction History Layout 00:message/user:BEFORE_SWITCH_USER 01:message/user:\nPRETURN_SWITCH_SUMMARY -02:message/developer:\nThe user was previously using a different model.... -03:message/developer: -04:message/developer: The user has requested a new communication st... -05:message/user: -06:message/user:> -07:message/user:AFTER_SWITCH_USER +02:message/developer[3]: + [01] \nThe user was previously using a different model.... + [02] + [03] The user has requested a new communication st... +03:message/user[2]: + [01] + [02] > +04:message/user:AFTER_SWITCH_USER diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap index 701e676761..83bec30fb6 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap @@ -6,14 +6,16 @@ Scenario: Remote manual /compact where remote compact output is compaction-only: ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:hello remote compact -04:message/assistant:FIRST_REMOTE_REPLY +01:message/user[2]: + [01] + [02] > +02:message/user:hello remote compact +03:message/assistant:FIRST_REMOTE_REPLY ## Remote Post-Compaction History Layout 00:compaction:encrypted=true 01:message/developer: -02:message/user: -03:message/user:> -04:message/user:after compact +02:message/user[2]: + [01] + [02] > +03:message/user:after compact diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap index ce62806d99..6ec8149c07 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap @@ -6,11 +6,13 @@ Scenario: Remote manual /compact with no prior user turn still issues a compact ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> +01:message/user[2]: + [01] + [02] > ## Remote Post-Compaction History Layout 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap index 7d5bcde902..ccc5a55811 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap @@ -1,6 +1,5 @@ --- source: core/tests/suite/compact_remote.rs -assertion_line: 1876 expression: "format_labeled_requests_snapshot(\"After a prior manual /compact produced an older remote compaction item, the next turn hits remote auto-compaction before the next sampling request. The compact request carries forward that earlier compaction item, and the next sampling request shows the latest compaction item with context reinjected before USER_TWO.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Second Turn Request (After Compaction)\", &second_turn_request),])" --- Scenario: After a prior manual /compact produced an older remote compaction item, the next turn hits remote auto-compaction before the next sampling request. The compact request carries forward that earlier compaction item, and the next sampling request shows the latest compaction item with context reinjected before USER_TWO. @@ -13,6 +12,7 @@ Scenario: After a prior manual /compact produced an older remote compaction item 00:message/user:USER_ONE 01:compaction:encrypted=true 02:message/developer: -03:message/user: -04:message/user:> -05:message/user:USER_TWO +03:message/user[2]: + [01] + [02] > +04:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap index f5533b94a9..8e3e4235fa 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap @@ -6,15 +6,17 @@ Scenario: Remote mid-turn continuation compaction after tool output: compact req ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:function_call/test_tool -05:function_call_output:unsupported call: test_tool +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:function_call/test_tool +04:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:compaction:encrypted=true +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap index 3f8c77e975..0f5886be13 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap @@ -1,19 +1,21 @@ --- source: core/tests/suite/compact_remote.rs -expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])" +expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])" --- Scenario: Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item. ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:function_call/test_tool -05:function_call_output:unsupported call: test_tool +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:function_call/test_tool +04:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout 00:message/developer: -01:message/user: -02:message/user:> -03:compaction:encrypted=true +01:message/user[2]: + [01] + [02] > +02:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap index e718bfda88..88e5c0bd23 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap @@ -6,7 +6,8 @@ Scenario: Remote pre-turn auto-compaction context-window failure: compaction req ## Remote Compaction Request (Incoming User Excluded) 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:message/assistant:REMOTE_FIRST_REPLY +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:message/assistant:REMOTE_FIRST_REPLY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap index f8aa74e9a1..224f6dbba7 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap @@ -6,7 +6,8 @@ Scenario: Remote pre-turn auto-compaction parse failure: compaction request excl ## Remote Compaction Request (Incoming User Excluded) 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:turn that exceeds token threshold -04:message/assistant:initial turn complete +01:message/user[2]: + [01] + [02] > +02:message/user:turn that exceeds token threshold +03:message/assistant:initial turn complete diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap index 2236984a67..5a6f270d3d 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap @@ -6,18 +6,20 @@ Scenario: Remote pre-turn auto-compaction with a context override emits the cont ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:USER_ONE -04:message/assistant:REMOTE_FIRST_REPLY -05:message/user:USER_TWO -06:message/assistant:REMOTE_SECOND_REPLY +01:message/user[2]: + [01] + [02] > +02:message/user:USER_ONE +03:message/assistant:REMOTE_FIRST_REPLY +04:message/user:USER_TWO +05:message/assistant:REMOTE_SECOND_REPLY ## Remote Post-Compaction History Layout 00:message/user:USER_ONE 01:message/user:USER_TWO 02:compaction:encrypted=true 03:message/developer: -04:message/user: -05:message/user: -06:message/user:USER_THREE +04:message/user[2]: + [01] + [02] +05:message/user:USER_THREE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap index 15beec5f27..ebab84f4e0 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap @@ -1,28 +1,33 @@ --- source: core/tests/suite/compact_remote.rs +assertion_line: 1514 expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming from the compact request payload, and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &initial_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])" --- Scenario: Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming from the compact request payload, and restores it in the post-compaction follow-up request. ## Initial Request (Previous Model) 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:BEFORE_SWITCH_USER +01:message/user[2]: + [01] + [02] > +02:message/user:BEFORE_SWITCH_USER ## Remote Compaction Request 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:BEFORE_SWITCH_USER -04:message/assistant:BEFORE_SWITCH_REPLY +01:message/user[2]: + [01] + [02] > +02:message/user:BEFORE_SWITCH_USER +03:message/assistant:BEFORE_SWITCH_REPLY ## Remote Post-Compaction History Layout 00:message/user:BEFORE_SWITCH_USER 01:compaction:encrypted=true -02:message/developer:\nThe user was previously using a different model.... -03:message/developer: -04:message/developer: The user has requested a new communication st... -05:message/user: -06:message/user:> -07:message/user:AFTER_SWITCH_USER +02:message/developer[3]: + [01] \nThe user was previously using a different model.... + [02] + [03] The user has requested a new communication st... +03:message/user[2]: + [01] + [02] > +04:message/user:AFTER_SWITCH_USER diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap index 9b6291809a..65dffc556c 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap @@ -6,19 +6,21 @@ Scenario: Second turn changes cwd to a directory with different AGENTS.md; curre ## First Request (agents_one) 00:message/developer: -01:message/user: -02:message/user:> +01:message/user[2]: + [01] + [02] > +02:message/developer: 03:message/user:> -04:message/developer: -05:message/user:first turn in agents_one +04:message/user:first turn in agents_one ## Second Request (agents_two cwd) 00:message/developer: -01:message/user: -02:message/user:> +01:message/user[2]: + [01] + [02] > +02:message/developer: 03:message/user:> -04:message/developer: -05:message/user:first turn in agents_one -06:message/assistant:turn one complete -07:message/user:> -08:message/user:second turn in agents_two +04:message/user:first turn in agents_one +05:message/assistant:turn one complete +06:message/user:> +07:message/user:second turn in agents_two diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_one_subagent.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_one_subagent.snap new file mode 100644 index 0000000000..3436943cd2 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_one_subagent.snap @@ -0,0 +1,6 @@ +--- +source: core/tests/suite/model_visible_layout.rs +assertion_line: 476 +expression: "format_environment_context_subagents_snapshot(&[\"- agent-1: Atlas\"])" +--- +00:message/user::subagents=1> diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_two_subagents.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_two_subagents.snap new file mode 100644 index 0000000000..105c28515b --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_environment_context_includes_two_subagents.snap @@ -0,0 +1,6 @@ +--- +source: core/tests/suite/model_visible_layout.rs +assertion_line: 486 +expression: "format_environment_context_subagents_snapshot(&[\"- agent-1: Atlas\",\n\"- agent-2: Juniper\",])" +--- +00:message/user::subagents=2> diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap index 06269eff76..045e97706b 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap @@ -1,21 +1,22 @@ --- source: core/tests/suite/model_visible_layout.rs -assertion_line: 435 expression: "format_labeled_requests_snapshot(\"First post-resume turn where pre-turn override sets model to rollout model; no model-switch update should appear.\",\n&[(\"Last Request Before Resume\", &initial_request),\n(\"First Request After Resume + Override\", &resumed_request),])" --- Scenario: First post-resume turn where pre-turn override sets model to rollout model; no model-switch update should appear. ## Last Request Before Resume 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:seed resume history +01:message/user[2]: + [01] + [02] > +02:message/user:seed resume history ## First Request After Resume + Override 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:seed resume history -04:message/assistant:recorded before resume -05:message/user: -06:message/user:first resumed turn after model override +01:message/user[2]: + [01] + [02] > +02:message/user:seed resume history +03:message/assistant:recorded before resume +04:message/user: +05:message/user:first resumed turn after model override diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap index 54417c6de2..3918fafa65 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap @@ -1,23 +1,25 @@ --- source: core/tests/suite/model_visible_layout.rs -assertion_line: 337 expression: "format_labeled_requests_snapshot(\"First post-resume turn where resumed config model differs from rollout and personality changes.\",\n&[(\"Last Request Before Resume\", &initial_request),\n(\"First Request After Resume\", &resumed_request),])" --- Scenario: First post-resume turn where resumed config model differs from rollout and personality changes. ## Last Request Before Resume 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:seed resume history +01:message/user[2]: + [01] + [02] > +02:message/user:seed resume history ## First Request After Resume 00:message/developer: -01:message/user: -02:message/user:> -03:message/user:seed resume history -04:message/assistant:recorded before resume -05:message/developer:\nThe user was previously using a different model. Please continue the conversatio... -06:message/user: -07:message/developer: -08:message/user:resume and change personality +01:message/user[2]: + [01] + [02] > +02:message/user:seed resume history +03:message/assistant:recorded before resume +04:message/developer[2]: + [01] \nThe user was previously using a different model. Please continue the conversatio... + [02] +05:message/user: +06:message/user:resume and change personality diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap index 9ec0b2e7d0..2172d7399f 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap @@ -6,19 +6,22 @@ Scenario: Second turn changes cwd, approval policy, and personality while keepin ## First Request (Baseline) 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:first turn +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:first turn ## Second Request (Turn Overrides) 00:message/developer: -01:message/user: -02:message/user:> -03:message/developer: -04:message/user:first turn -05:message/assistant:turn one complete +01:message/user[2]: + [01] + [02] > +02:message/developer: +03:message/user:first turn +04:message/assistant:turn one complete +05:message/developer[2]: + [01] + [02] The user has requested a new communication style. Future messages should adhe... 06:message/user: -07:message/developer: -08:message/developer: The user has requested a new communication style. Future messages should adhe... -09:message/user:second turn with context updates +07:message/user:second turn with context updates diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 422510f328..3dc463c4ab 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -20,6 +20,8 @@ use tokio::time::sleep; use wiremock::MockServer; const SPAWN_CALL_ID: &str = "spawn-call-1"; +const FORKED_SPAWN_AGENT_OUTPUT_MESSAGE: &str = "You are the newly spawned agent. The prior conversation history was forked from your parent agent. Treat the next user message as your new task, and use the forked history only as background context."; +const TURN_0_FORK_PROMPT: &str = "seed fork context"; const TURN_1_PROMPT: &str = "spawn a child and continue"; const TURN_2_NO_WAIT_PROMPT: &str = "follow up without wait"; const CHILD_PROMPT: &str = "child: do work"; @@ -194,3 +196,116 @@ async fn subagent_notification_is_included_without_wait() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn spawned_child_receives_forked_parent_context() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let seed_turn = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, TURN_0_FORK_PROMPT), + sse(vec![ + ev_response_created("resp-seed-1"), + ev_assistant_message("msg-seed-1", "seeded"), + ev_completed("resp-seed-1"), + ]), + ) + .await; + + let spawn_args = serde_json::to_string(&json!({ + "message": CHILD_PROMPT, + "fork_context": true, + }))?; + let spawn_turn = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), + sse(vec![ + ev_response_created("resp-turn1-1"), + ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args), + ev_completed("resp-turn1-1"), + ]), + ) + .await; + + let _child_request_log = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, CHILD_PROMPT), + sse(vec![ + ev_response_created("resp-child-1"), + ev_assistant_message("msg-child-1", "child done"), + ev_completed("resp-child-1"), + ]), + ) + .await; + + let _turn1_followup = mount_sse_once_match( + &server, + |req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID), + sse(vec![ + ev_response_created("resp-turn1-2"), + ev_assistant_message("msg-turn1-2", "parent done"), + ev_completed("resp-turn1-2"), + ]), + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::Collab); + }); + let test = builder.build(&server).await?; + + test.submit_turn(TURN_0_FORK_PROMPT).await?; + let _ = seed_turn.single_request(); + + test.submit_turn(TURN_1_PROMPT).await?; + let _ = spawn_turn.single_request(); + + let deadline = Instant::now() + Duration::from_secs(2); + let child_request = loop { + if let Some(request) = server + .received_requests() + .await + .unwrap_or_default() + .into_iter() + .find(|request| { + body_contains(request, CHILD_PROMPT) + && body_contains(request, FORKED_SPAWN_AGENT_OUTPUT_MESSAGE) + }) + { + break request; + } + if Instant::now() >= deadline { + anyhow::bail!("timed out waiting for forked child request"); + } + sleep(Duration::from_millis(10)).await; + }; + assert!(body_contains(&child_request, TURN_0_FORK_PROMPT)); + assert!(body_contains(&child_request, "seeded")); + + let child_body = child_request + .body_json::() + .expect("forked child request body should be json"); + let function_call_output = child_body["input"] + .as_array() + .and_then(|items| { + items.iter().find(|item| { + item["type"].as_str() == Some("function_call_output") + && item["call_id"].as_str() == Some(SPAWN_CALL_ID) + }) + }) + .unwrap_or_else(|| panic!("expected forked child request to include spawn_agent output")); + let (content, success) = match &function_call_output["output"] { + serde_json::Value::String(text) => (Some(text.as_str()), None), + serde_json::Value::Object(output) => ( + output.get("content").and_then(serde_json::Value::as_str), + output.get("success").and_then(serde_json::Value::as_bool), + ), + _ => (None, None), + }; + assert_eq!(content, Some(FORKED_SPAWN_AGENT_OUTPUT_MESSAGE)); + assert_ne!(success, Some(false)); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 507c8eb068..13191892ce 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -4,7 +4,6 @@ use std::fs; use assert_matches::assert_matches; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::plan_tool::StepStatus; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -89,7 +88,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -158,7 +157,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -237,7 +236,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -328,7 +327,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -427,7 +426,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index 357dcff0fd..1678e58349 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -5,7 +5,6 @@ use std::fs; use std::time::Duration; use std::time::Instant; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -46,7 +45,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -361,7 +360,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 8ede6c621f..6ba6e53b1b 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -5,7 +5,6 @@ use anyhow::Context; use anyhow::Result; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -371,6 +370,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -465,6 +465,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config @@ -488,7 +489,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -730,6 +731,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }, ); config diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 705e415620..55bc19e680 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -6,7 +6,6 @@ use std::sync::OnceLock; use anyhow::Context; use anyhow::Result; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandSource; @@ -209,7 +208,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -338,7 +337,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -416,7 +415,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -497,7 +496,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -590,7 +589,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -665,7 +664,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -741,7 +740,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -871,7 +870,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1008,7 +1007,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1168,7 +1167,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1266,7 +1265,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1384,7 +1383,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1474,7 +1473,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1555,7 +1554,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1686,7 +1685,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1854,7 +1853,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<() sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -1931,7 +1930,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2019,7 +2018,7 @@ async fn unified_exec_interrupt_terminates_long_running_session() -> Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2116,7 +2115,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2251,7 +2250,7 @@ PY sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2365,7 +2364,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2461,7 +2460,7 @@ PY sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2543,7 +2542,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2647,7 +2646,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2742,7 +2741,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -2877,7 +2876,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index 5ef939776b..a2303c0ecf 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -23,10 +23,6 @@ use responses::start_mock_server; use std::time::Duration; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "flaky on ubuntu-24.04-arm - aarch64-unknown-linux-gnu"] -// The notify script gets far enough to create (and therefore surface) the file, -// but hasn’t flushed the JSON yet. Reading an empty file produces EOF while parsing -// a value at line 1 column 0. May be caused by a slow runner. async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -43,7 +39,10 @@ async fn summarize_context_three_requests_and_instructions() -> anyhow::Result<( ¬ify_script, r#"#!/bin/bash set -e -echo -n "${@: -1}" > $(dirname "${0}")/notify.txt"#, +payload_path="$(dirname "${0}")/notify.txt" +tmp_path="${payload_path}.tmp" +echo -n "${@: -1}" > "${tmp_path}" +mv "${tmp_path}" "${payload_path}""#, )?; std::fs::set_permissions(¬ify_script, std::fs::Permissions::from_mode(0o755))?; diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index c38b86e443..766d79abb8 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -1,6 +1,5 @@ use anyhow::Context; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandEndEvent; @@ -178,7 +177,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: fixture.session_configured.model.clone(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 5d2c8e3d5c..4521ed45f8 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -36,28 +36,37 @@ use image::GenericImageView; use image::ImageBuffer; use image::Rgba; use image::load_from_memory; +use pretty_assertions::assert_eq; use serde_json::Value; use tokio::time::Duration; use wiremock::BodyPrintLimit; use wiremock::MockServer; -fn find_image_message(body: &Value) -> Option<&Value> { +fn image_messages(body: &Value) -> Vec<&Value> { body.get("input") .and_then(Value::as_array) - .and_then(|items| { - items.iter().find(|item| { - item.get("type").and_then(Value::as_str) == Some("message") - && item - .get("content") - .and_then(Value::as_array) - .map(|content| { - content.iter().any(|span| { - span.get("type").and_then(Value::as_str) == Some("input_image") + .map(|items| { + items + .iter() + .filter(|item| { + item.get("type").and_then(Value::as_str) == Some("message") + && item + .get("content") + .and_then(Value::as_array) + .map(|content| { + content.iter().any(|span| { + span.get("type").and_then(Value::as_str) == Some("input_image") + }) }) - }) - .unwrap_or(false) - }) + .unwrap_or(false) + }) + .collect() }) + .unwrap_or_default() +} + +fn find_image_message(body: &Value) -> Option<&Value> { + image_messages(body).into_iter().next() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -103,7 +112,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -205,7 +214,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -335,7 +344,7 @@ console.log(out.output?.body?.text ?? ""); sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -349,38 +358,26 @@ console.log(out.output?.body?.text ?? ""); .await; let req = mock.single_request(); - let (js_repl_output, js_repl_success) = req - .custom_tool_call_output_content_and_success(call_id) - .expect("custom tool output present"); - let js_repl_output = js_repl_output.expect("custom tool output text present"); - if js_repl_output.contains("Node runtime not found") - || js_repl_output.contains("Node runtime too old for js_repl") - { - eprintln!("Skipping js_repl image test: {js_repl_output}"); - return Ok(()); - } - assert_ne!( - js_repl_success, - Some(false), - "js_repl call failed unexpectedly: {js_repl_output}" + let body = req.body_json(); + assert_eq!( + image_messages(&body).len(), + 0, + "js_repl view_image should not inject a pending input image message" ); - let body = req.body_json(); - let image_message = - find_image_message(&body).expect("pending input image message not included in request"); - let image_url = image_message - .get("content") + let custom_output = req.custom_tool_call_output(call_id); + let output_items = custom_output + .get("output") .and_then(Value::as_array) - .and_then(|content| { - content.iter().find_map(|span| { - if span.get("type").and_then(Value::as_str) == Some("input_image") { - span.get("image_url").and_then(Value::as_str) - } else { - None - } - }) + .expect("custom_tool_call_output should be a content item array"); + let image_url = output_items + .iter() + .find_map(|item| { + (item.get("type").and_then(Value::as_str) == Some("input_image")) + .then(|| item.get("image_url").and_then(Value::as_str)) + .flatten() }) - .expect("image_url present"); + .expect("image_url present in js_repl custom tool output"); assert!( image_url.starts_with("data:image/png;base64,"), "expected png data URL, got {image_url}" @@ -436,7 +433,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -511,7 +508,7 @@ async fn view_image_tool_placeholder_for_non_image_files() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -603,7 +600,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -664,8 +661,10 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an base_instructions: "base instructions".to_string(), model_messages: None, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -725,7 +724,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an sandbox_policy: SandboxPolicy::DangerFullAccess, model: model_slug.to_string(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) @@ -801,7 +800,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_model, effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index 20c7b3b2f2..9ff5cbe001 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -1,6 +1,5 @@ use anyhow::Result; use codex_core::features::Feature; -use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -161,7 +160,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result sandbox_policy: SandboxPolicy::DangerFullAccess, model: session_configured.model.clone(), effort: None, - summary: ReasoningSummary::Auto, + summary: None, collaboration_mode: None, personality: None, }) diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 7cf80e3d86..762c11cb63 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -181,6 +181,7 @@ impl AppServerClient { source_kinds: None, archived: None, cwd: None, + search_term: None, }, }; self.send(&request)?; diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index 503891a491..763f2602ad 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -81,7 +81,8 @@ Interrupt a running turn: `interruptConversation`. List/resume/archive: `listConversations`, `resumeConversation`, `archiveConversation`. -For v2 threads, use `thread/list` with filters such as `archived: true` or `cwd: "/path"` to +For v2 threads, use `thread/list` with filters such as `archived: true`, `cwd: "/path"`, or +`searchTerm: "needle"` to narrow results, and `thread/unarchive` to restore archived rollouts to the active sessions directory (it returns the restored thread summary). @@ -89,20 +90,26 @@ directory (it returns the restored thread summary). Fetch the catalog of models available in the current Codex build with `model/list`. The request accepts optional pagination inputs: -- `pageSize` – number of models to return (defaults to a server-selected value) +- `limit` – number of models to return (defaults to a server-selected value) - `cursor` – opaque string from the previous response’s `nextCursor` Each response yields: -- `items` – ordered list of models. A model includes: +- `data` – ordered list of models. A model includes: - `id`, `model`, `displayName`, `description` - `supportedReasoningEfforts` – array of objects with: - - `reasoningEffort` – one of `minimal|low|medium|high` + - `reasoningEffort` – one of `none|minimal|low|medium|high|xhigh` - `description` – human-friendly label for the effort - `defaultReasoningEffort` – suggested effort for the UI + - `inputModalities` – accepted input types for the model - `supportsPersonality` – whether the model supports personality-specific instructions - `isDefault` – whether the model is recommended for most users - `upgrade` – optional recommended upgrade model id + - `upgradeInfo` – optional upgrade metadata object with: + - `model` – recommended upgrade model id + - `upgradeCopy` – optional display copy for the upgrade recommendation + - `modelLink` – optional link for the upgrade recommendation + - `migrationMarkdown` – optional markdown shown when presenting the upgrade - `nextCursor` – pass into the next request to continue paging (optional) ## Collaboration modes (experimental) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 6e679052e0..a3a29852b3 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -854,7 +854,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::RealtimeConversationRealtime(_) | EventMsg::RealtimeConversationClosed(_) | EventMsg::DynamicToolCallRequest(_) - | EventMsg::SkillRequestApproval(_) => {} + | EventMsg::DynamicToolCallResponse(_) => {} } CodexStatus::Running } @@ -925,6 +925,7 @@ impl EventProcessorWithHumanOutput { | EventMsg::ThreadRolledBack(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) ) } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 382081dd3d..e8ab6f8af7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -13,6 +13,7 @@ pub mod exec_events; pub use cli::Cli; pub use cli::Command; pub use cli::ReviewArgs; +use codex_arg0::Arg0DispatchPaths; use codex_cloud_requirements::cloud_requirements_loader; use codex_core::AuthManager; use codex_core::LMSTUDIO_OSS_PROVIDER_ID; @@ -31,6 +32,7 @@ use codex_core::config_loader::ConfigLoadError; use codex_core::config_loader::format_config_error_with_source; use codex_core::format_exec_policy_error_with_source; use codex_core::git_info::get_git_repo_root; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::SandboxMode; @@ -90,7 +92,7 @@ struct ThreadEventEnvelope { suppress_output: bool, } -pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { +pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { if let Err(err) = set_default_originator("codex_exec".to_string()) { tracing::warn!(?err, "Failed to set codex exec originator override {err:?}"); } @@ -271,7 +273,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any sandbox_mode, cwd: resolved_cwd, model_provider: model_provider.clone(), - codex_linux_sandbox_exe, + codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), js_repl_node_path: None, js_repl_node_module_dirs: None, zsh_path: None, @@ -374,7 +377,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let default_approval_policy = config.permissions.approval_policy.value(); let default_sandbox_policy = config.permissions.sandbox_policy.get(); let default_effort = config.model_reasoning_effort; - let default_summary = config.model_reasoning_summary; // When --yolo (dangerously_bypass_approvals_and_sandbox) is set, also skip the git repo check // since the user is explicitly running in an externally sandboxed environment. @@ -396,6 +398,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any auth_manager.clone(), SessionSource::Exec, config.model_catalog.clone(), + CollaborationModesConfig { + default_mode_request_user_input: config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + }, )); let default_model = thread_manager .get_models_manager() @@ -552,7 +559,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any sandbox_policy: default_sandbox_policy.clone(), model: default_model, effort: default_effort, - summary: default_summary, + summary: None, final_output_json_schema: output_schema, collaboration_mode: None, personality: None, @@ -694,6 +701,7 @@ fn should_suppress_agent_job_event(msg: &EventMsg) -> bool { | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::ElicitationRequest(_) | EventMsg::Error(_) | EventMsg::Warning(_) diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs index 9c6cf52f9f..e4cbd25ad2 100644 --- a/codex-rs/exec/src/main.rs +++ b/codex-rs/exec/src/main.rs @@ -10,6 +10,7 @@ //! This allows us to ship a completely separate set of functionality as part //! of the `codex-exec` binary. use clap::Parser; +use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_exec::Cli; use codex_exec::run_main; @@ -25,7 +26,7 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { let top_cli = TopCli::parse(); // Merge root-level overrides into inner CLI struct so downstream logic remains unchanged. let mut inner = top_cli.inner; @@ -34,7 +35,7 @@ fn main() -> anyhow::Result<()> { .raw_overrides .splice(0..0, top_cli.config_overrides.raw_overrides); - run_main(inner, codex_linux_sandbox_exe).await?; + run_main(inner, arg0_paths).await?; Ok(()) }) } diff --git a/codex-rs/exec/tests/suite/resume.rs b/codex-rs/exec/tests/suite/resume.rs index e5074da748..a85183620e 100644 --- a/codex-rs/exec/tests/suite/resume.rs +++ b/codex-rs/exec/tests/suite/resume.rs @@ -278,6 +278,11 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> { .assert() .success(); + // `resume --last` sorts by `updated_at`, which is second-granularity. Sleep so + // the upcoming `resume --last --all` write lands in a later second and becomes + // deterministically newest (instead of tying and falling back to UUID order). + std::thread::sleep(std::time::Duration::from_millis(1100)); + let marker_b2 = format!("resume-cwd-b-2-{}", Uuid::new_v4()); let prompt_b2 = format!("echo {marker_b2}"); test.cmd() @@ -316,6 +321,9 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> { let resumed_path_cwd = find_session_file_containing_marker(&sessions_dir, &marker_a2) .expect("no resumed session file containing marker_a2"); + // The `--all` resume above appends a new turn to `path_b` while running from `dir_a`, so the + // session's latest cwd now matches `dir_a`. A subsequent `resume --last` should therefore pick + // the newest matching session (`path_b`). assert_eq!( resumed_path_cwd, path_b, "resume --last should prefer sessions whose latest turn context matches the current cwd" diff --git a/codex-rs/hooks/src/registry.rs b/codex-rs/hooks/src/registry.rs index 6568f5374f..1648fa0462 100644 --- a/codex-rs/hooks/src/registry.rs +++ b/codex-rs/hooks/src/registry.rs @@ -104,6 +104,7 @@ mod tests { HookPayload { session_id: ThreadId::new(), cwd: PathBuf::from(CWD), + client: None, triggered_at: Utc .with_ymd_and_hms(2025, 1, 1, 0, 0, 0) .single() @@ -172,6 +173,7 @@ mod tests { HookPayload { session_id: ThreadId::new(), cwd: PathBuf::from(CWD), + client: None, triggered_at: Utc .with_ymd_and_hms(2025, 1, 1, 0, 0, 0) .single() diff --git a/codex-rs/hooks/src/types.rs b/codex-rs/hooks/src/types.rs index 5131580128..b7ef3c2141 100644 --- a/codex-rs/hooks/src/types.rs +++ b/codex-rs/hooks/src/types.rs @@ -65,6 +65,8 @@ impl Hook { pub struct HookPayload { pub session_id: ThreadId, pub cwd: PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + pub client: Option, #[serde(serialize_with = "serialize_triggered_at")] pub triggered_at: DateTime, pub hook_event: HookEvent, @@ -181,6 +183,7 @@ mod tests { let payload = HookPayload { session_id, cwd: PathBuf::from("tmp"), + client: None, triggered_at: Utc .with_ymd_and_hms(2025, 1, 1, 0, 0, 0) .single() @@ -218,6 +221,7 @@ mod tests { let payload = HookPayload { session_id, cwd: PathBuf::from("tmp"), + client: None, triggered_at: Utc .with_ymd_and_hms(2025, 1, 1, 0, 0, 0) .single() diff --git a/codex-rs/hooks/src/user_notification.rs b/codex-rs/hooks/src/user_notification.rs index 50472225e4..caeca59b50 100644 --- a/codex-rs/hooks/src/user_notification.rs +++ b/codex-rs/hooks/src/user_notification.rs @@ -1,4 +1,3 @@ -use std::path::Path; use std::process::Stdio; use std::sync::Arc; @@ -19,6 +18,8 @@ enum UserNotification { thread_id: String, turn_id: String, cwd: String, + #[serde(skip_serializing_if = "Option::is_none")] + client: Option, /// Messages that the user sent to the agent to initiate the turn. input_messages: Vec, @@ -28,13 +29,14 @@ enum UserNotification { }, } -pub fn legacy_notify_json(hook_event: &HookEvent, cwd: &Path) -> Result { - match hook_event { +pub fn legacy_notify_json(payload: &HookPayload) -> Result { + match &payload.hook_event { HookEvent::AfterAgent { event } => { serde_json::to_string(&UserNotification::AgentTurnComplete { thread_id: event.thread_id.to_string(), turn_id: event.turn_id.clone(), - cwd: cwd.display().to_string(), + cwd: payload.cwd.display().to_string(), + client: payload.client.clone(), input_messages: event.input_messages.clone(), last_assistant_message: event.last_assistant_message.clone(), }) @@ -56,7 +58,7 @@ pub fn notify_hook(argv: Vec) -> Hook { Some(command) => command, None => return HookResult::Success, }; - if let Ok(notify_payload) = legacy_notify_json(&payload.hook_event, &payload.cwd) { + if let Ok(notify_payload) = legacy_notify_json(payload) { command.arg(notify_payload); } @@ -91,6 +93,7 @@ mod tests { "thread-id": "b5f6c1c2-1111-2222-3333-444455556666", "turn-id": "12345", "cwd": "/Users/example/project", + "client": "codex-tui", "input-messages": ["Rename `foo` to `bar` and update the callsites."], "last-assistant-message": "Rename complete and verified `cargo build` succeeds.", }) @@ -102,6 +105,7 @@ mod tests { thread_id: "b5f6c1c2-1111-2222-3333-444455556666".to_string(), turn_id: "12345".to_string(), cwd: "/Users/example/project".to_string(), + client: Some("codex-tui".to_string()), input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()], last_assistant_message: Some( "Rename complete and verified `cargo build` succeeds.".to_string(), @@ -115,19 +119,27 @@ mod tests { #[test] fn legacy_notify_json_matches_historical_wire_shape() -> Result<()> { - let hook_event = HookEvent::AfterAgent { - event: crate::HookEventAfterAgent { - thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666") - .expect("valid thread id"), - turn_id: "12345".to_string(), - input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()], - last_assistant_message: Some( - "Rename complete and verified `cargo build` succeeds.".to_string(), - ), + let payload = HookPayload { + session_id: ThreadId::new(), + cwd: std::path::Path::new("/Users/example/project").to_path_buf(), + client: Some("codex-tui".to_string()), + triggered_at: chrono::Utc::now(), + hook_event: HookEvent::AfterAgent { + event: crate::HookEventAfterAgent { + thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666") + .expect("valid thread id"), + turn_id: "12345".to_string(), + input_messages: vec![ + "Rename `foo` to `bar` and update the callsites.".to_string(), + ], + last_assistant_message: Some( + "Rename complete and verified `cargo build` succeeds.".to_string(), + ), + }, }, }; - let serialized = legacy_notify_json(&hook_event, Path::new("/Users/example/project"))?; + let serialized = legacy_notify_json(&payload)?; let actual: Value = serde_json::from_str(&serialized)?; assert_eq!(actual, expected_notification_json()); diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index b9c0d6c8b6..362fcaf35f 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -73,7 +73,6 @@ async fn run_cmd_result_with_writable_roots( let sandbox_cwd = cwd.clone(); let params = ExecParams { command: cmd.iter().copied().map(str::to_owned).collect(), - original_command: cmd.iter().copied().map(str::to_owned).collect(), cwd, expiration: timeout_ms.into(), env: create_env_from_core_vars(), @@ -316,7 +315,6 @@ async fn assert_network_blocked(cmd: &[&str]) { let sandbox_cwd = cwd.clone(); let params = ExecParams { command: cmd.iter().copied().map(str::to_owned).collect(), - original_command: cmd.iter().copied().map(str::to_owned).collect(), cwd, // Give the tool a generous 2-second timeout so even slow DNS timeouts // do not stall the suite. diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 3125221d1a..f3f66b5559 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -1,5 +1,6 @@ //! Configuration object accepted by the `codex` MCP tool-call. +use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_protocol::ThreadId; @@ -153,7 +154,7 @@ impl CodexToolCallParam { /// effective Config object generated from the supplied parameters. pub async fn into_config( self, - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, ) -> std::io::Result<(String, Config)> { let Self { prompt, @@ -175,7 +176,8 @@ impl CodexToolCallParam { cwd: cwd.map(PathBuf::from), approval_policy: approval_policy.map(Into::into), sandbox_mode: sandbox.map(Into::into), - codex_linux_sandbox_exe, + codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, compact_prompt, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index eff8d8e13e..e624fe2b1b 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -227,6 +227,7 @@ async fn run_codex_tool_session_inner( parsed_cmd, network_approval_context: _, additional_permissions: _, + available_decisions: _, } = ev; handle_exec_approval_request( command, @@ -363,7 +364,7 @@ async fn run_codex_tool_session_inner( | EventMsg::ExitedReviewMode(_) | EventMsg::RequestUserInput(_) | EventMsg::DynamicToolCallRequest(_) - | EventMsg::SkillRequestApproval(_) + | EventMsg::DynamicToolCallResponse(_) | EventMsg::ContextCompacted(_) | EventMsg::ModelReroute(_) | EventMsg::ThreadRolledBack(_) diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index fd4475a61d..554daf9a8c 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -3,8 +3,8 @@ use std::io::ErrorKind; use std::io::Result as IoResult; -use std::path::PathBuf; +use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_utils_cli::CliConfigOverrides; @@ -49,7 +49,7 @@ const CHANNEL_CAPACITY: usize = 128; type IncomingMessage = JsonRpcMessage; pub async fn run_main( - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, cli_config_overrides: CliConfigOverrides, ) -> IoResult<()> { // Install a simple subscriber so `tracing` output is visible. Users can @@ -105,7 +105,7 @@ pub async fn run_main( let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); let mut processor = MessageProcessor::new( outgoing_message_sender, - codex_linux_sandbox_exe, + arg0_paths, std::sync::Arc::new(config), ); async move { diff --git a/codex-rs/mcp-server/src/main.rs b/codex-rs/mcp-server/src/main.rs index c6ffd49b40..ce61fd04d1 100644 --- a/codex-rs/mcp-server/src/main.rs +++ b/codex-rs/mcp-server/src/main.rs @@ -1,10 +1,11 @@ +use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_mcp_server::run_main; use codex_utils_cli::CliConfigOverrides; fn main() -> anyhow::Result<()> { - arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { - run_main(codex_linux_sandbox_exe, CliConfigOverrides::default()).await?; + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { + run_main(arg0_paths, CliConfigOverrides::default()).await?; Ok(()) }) } diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 95d48aa822..6b634e1947 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; -use std::path::PathBuf; +use codex_arg0::Arg0DispatchPaths; use codex_core::AuthManager; use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::Submission; @@ -38,7 +39,7 @@ use crate::outgoing_message::OutgoingMessageSender; pub(crate) struct MessageProcessor { outgoing: Arc, initialized: bool, - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, thread_manager: Arc, running_requests_id_to_codex_uuid: Arc>>, } @@ -48,7 +49,7 @@ impl MessageProcessor { /// `Sender` so handlers can enqueue messages to be written to stdout. pub(crate) fn new( outgoing: OutgoingMessageSender, - codex_linux_sandbox_exe: Option, + arg0_paths: Arg0DispatchPaths, config: Arc, ) -> Self { let outgoing = Arc::new(outgoing); @@ -62,11 +63,16 @@ impl MessageProcessor { auth_manager, SessionSource::Mcp, config.model_catalog.clone(), + CollaborationModesConfig { + default_mode_request_user_input: config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + }, )); Self { outgoing, initialized: false, - codex_linux_sandbox_exe, + arg0_paths, thread_manager, running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())), } @@ -352,10 +358,7 @@ impl MessageProcessor { let arguments = arguments.map(serde_json::Value::Object); let (initial_prompt, config): (String, Config) = match arguments { Some(json_val) => match serde_json::from_value::(json_val) { - Ok(tool_cfg) => match tool_cfg - .into_config(self.codex_linux_sandbox_exe.clone()) - .await - { + Ok(tool_cfg) => match tool_cfg.into_config(self.arg0_paths.clone()).await { Ok(cfg) => cfg, Err(e) => { let result = CallToolResult { diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index cff7adf354..6ef6c690ad 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -15,6 +15,7 @@ workspace = true anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } +chrono = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-rustls-provider = { workspace = true } diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index d5575bc9a2..616a0341bf 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -137,6 +137,45 @@ the decider can auto-allow network requests originating from that command. **Important:** Explicit deny rules still win. The decider only gets a chance to override `not_allowed` (allowlist misses), not `denied` or `not_allowed_local`. +## OTEL Audit Events (embedded/managed) + +When `codex-network-proxy` is embedded in managed Codex runtime, policy decisions emit structured +OTEL-compatible events with `target=codex_otel.network_proxy`. + +Event name: + +- `codex.network_proxy.policy_decision` + - emitted for each policy decision (`domain` and `non_domain`). + - `network.policy.scope = "domain"` for host-policy evaluations (`evaluate_host_policy`). + - `network.policy.scope = "non_domain"` for mode-guard/proxy-state checks (including unix-socket guard paths and unix-socket allow decisions). + +Common fields: + +- `event.name` +- `event.timestamp` (RFC3339 UTC, millisecond precision) +- optional metadata: + - `conversation.id` + - `app.version` + - `user.account_id` +- policy/network: + - `network.policy.scope` (`domain` or `non_domain`) + - `network.policy.decision` (`allow`, `deny`, or `ask`) + - `network.policy.source` (`baseline_policy`, `mode_guard`, `proxy_state`, `decider`) + - `network.policy.reason` + - `network.transport.protocol` + - `server.address` + - `server.port` + - `http.request.method` (defaults to `"none"` when absent) + - `client.address` (defaults to `"unknown"` when absent) + - `network.policy.override` (`true` only when decider-allow overrides baseline `not_allowed`) + +Unix-socket block-path audits use sentinel endpoint values: + +- `server.address = "unix-socket"` +- `server.port = 0` + +Audit events intentionally avoid logging full URL/path/query data. + ## Admin API The admin API is a small HTTP server intended for debugging and runtime adjustments. diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index 465a0e591f..cc08cd3b25 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -1,5 +1,6 @@ use crate::config::NetworkMode; use crate::mitm; +use crate::network_policy::BlockDecisionAuditEventArgs; use crate::network_policy::NetworkDecision; use crate::network_policy::NetworkDecisionSource; use crate::network_policy::NetworkPolicyDecider; @@ -7,12 +8,15 @@ use crate::network_policy::NetworkPolicyDecision; use crate::network_policy::NetworkPolicyRequest; use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; +use crate::network_policy::emit_allow_decision_audit_event; +use crate::network_policy::emit_block_decision_audit_event; use crate::network_policy::evaluate_host_policy; use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_MITM_REQUIRED; use crate::reasons::REASON_NOT_ALLOWED; use crate::reasons::REASON_PROXY_DISABLED; +use crate::reasons::REASON_UNIX_SOCKET_UNSUPPORTED; use crate::responses::PolicyDecisionDetails; use crate::responses::blocked_header_value; use crate::responses::blocked_message_with_policy; @@ -176,6 +180,7 @@ async fn http_connect_accept( client_addr(&req), Some("CONNECT".to_string()), NetworkProtocol::HttpsConnect, + None, ) .await); } @@ -247,6 +252,18 @@ async fn http_connect_accept( if mode == NetworkMode::Limited && mitm_state.is_none() { // Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the // inner HTTP method/headers from the proxy, effectively bypassing method policy. + emit_http_block_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ModeGuard, + reason: REASON_MITM_REQUIRED, + protocol: NetworkProtocol::HttpsConnect, + server_address: host.as_str(), + server_port: authority.port, + method: Some("CONNECT"), + client_addr: client.as_deref(), + }, + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_MITM_REQUIRED, @@ -449,10 +466,23 @@ async fn http_plain_proxy( client_addr(&req), Some(req.method().as_str().to_string()), NetworkProtocol::Http, + Some(("unix-socket", 0)), ) .await); } if !method_allowed { + emit_http_block_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ModeGuard, + reason: REASON_METHOD_NOT_ALLOWED, + protocol: NetworkProtocol::Http, + server_address: "unix-socket", + server_port: 0, + method: Some(req.method().as_str()), + client_addr: client.as_deref(), + }, + ); let client = client.as_deref().unwrap_or_default(); let method = req.method(); warn!( @@ -462,6 +492,18 @@ async fn http_plain_proxy( } if !unix_socket_permissions_supported() { + emit_http_block_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ProxyState, + reason: REASON_UNIX_SOCKET_UNSUPPORTED, + protocol: NetworkProtocol::Http, + server_address: "unix-socket", + server_port: 0, + method: Some(req.method().as_str()), + client_addr: client.as_deref(), + }, + ); warn!("unix socket proxy unsupported on this platform (path={socket_path})"); return Ok(text_response( StatusCode::NOT_IMPLEMENTED, @@ -471,6 +513,18 @@ async fn http_plain_proxy( return match app_state.is_unix_socket_allowed(&socket_path).await { Ok(true) => { + emit_http_allow_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ProxyState, + reason: "allow", + protocol: NetworkProtocol::Http, + server_address: "unix-socket", + server_port: 0, + method: Some(req.method().as_str()), + client_addr: client.as_deref(), + }, + ); let client = client.as_deref().unwrap_or_default(); info!("unix socket allowed (client={client}, path={socket_path})"); match proxy_via_unix_socket(req, &socket_path).await { @@ -485,6 +539,18 @@ async fn http_plain_proxy( } } Ok(false) => { + emit_http_block_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ProxyState, + reason: REASON_NOT_ALLOWED, + protocol: NetworkProtocol::Http, + server_address: "unix-socket", + server_port: 0, + method: Some(req.method().as_str()), + client_addr: client.as_deref(), + }, + ); let client = client.as_deref().unwrap_or_default(); warn!("unix socket blocked (client={client}, path={socket_path})"); Ok(json_blocked("unix-socket", REASON_NOT_ALLOWED, None)) @@ -528,6 +594,7 @@ async fn http_plain_proxy( client_addr(&req), Some(req.method().as_str().to_string()), NetworkProtocol::Http, + None, ) .await); } @@ -581,6 +648,18 @@ async fn http_plain_proxy( } if !method_allowed { + emit_http_block_decision_audit_event( + &app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ModeGuard, + reason: REASON_METHOD_NOT_ALLOWED, + protocol: NetworkProtocol::Http, + server_address: host.as_str(), + server_port: port, + method: Some(req.method().as_str()), + client_addr: client.as_deref(), + }, + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_METHOD_NOT_ALLOWED, @@ -794,7 +873,23 @@ async fn proxy_disabled_response( client: Option, method: Option, protocol: NetworkProtocol, + audit_endpoint_override: Option<(&str, u16)>, ) -> Response { + let (audit_server_address, audit_server_port) = + audit_endpoint_override.unwrap_or((host.as_str(), port)); + emit_http_block_decision_audit_event( + app_state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ProxyState, + reason: REASON_PROXY_DISABLED, + protocol, + server_address: audit_server_address, + server_port: audit_server_port, + method: method.as_deref(), + client_addr: client.as_deref(), + }, + ); + let blocked_host = host.clone(); let _ = app_state .record_blocked(BlockedRequest::new(BlockedRequestArgs { @@ -837,6 +932,20 @@ fn text_response(status: StatusCode, body: &str) -> Response { .unwrap_or_else(|_| Response::new(Body::from(body.to_string()))) } +fn emit_http_block_decision_audit_event( + app_state: &NetworkProxyState, + args: BlockDecisionAuditEventArgs<'_>, +) { + emit_block_decision_audit_event(app_state, args); +} + +fn emit_http_allow_decision_audit_event( + app_state: &NetworkProxyState, + args: BlockDecisionAuditEventArgs<'_>, +) { + emit_allow_decision_audit_event(app_state, args); +} + #[derive(Serialize)] struct BlockedResponse<'a> { status: &'static str, @@ -911,6 +1020,80 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } + #[tokio::test(flavor = "current_thread")] + async fn http_plain_proxy_blocks_unix_socket_when_method_not_allowed() { + let state = Arc::new(network_proxy_state_for_policy( + NetworkProxySettings::default(), + )); + state + .set_network_mode(NetworkMode::Limited) + .await + .expect("network mode should update"); + + let mut req = Request::builder() + .method(Method::POST) + .uri("http://example.com") + .header("x-unix-socket", "/tmp/test.sock") + .body(Body::empty()) + .expect("request should build"); + req.extensions_mut().insert(state); + + let response = http_plain_proxy(None, req).await.unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-method-policy" + ); + } + + #[tokio::test(flavor = "current_thread")] + async fn http_plain_proxy_rejects_unix_socket_when_not_allowlisted() { + let state = Arc::new(network_proxy_state_for_policy( + NetworkProxySettings::default(), + )); + + let mut req = Request::builder() + .method(Method::GET) + .uri("http://example.com") + .header("x-unix-socket", "/tmp/test.sock") + .body(Body::empty()) + .expect("request should build"); + req.extensions_mut().insert(state); + + let response = http_plain_proxy(None, req).await.unwrap(); + + if cfg!(target_os = "macos") { + assert_eq!(response.status(), StatusCode::FORBIDDEN); + assert_eq!( + response.headers().get("x-proxy-error").unwrap(), + "blocked-by-allowlist" + ); + } else { + assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED); + } + } + + #[cfg(target_os = "macos")] + #[tokio::test(flavor = "current_thread")] + async fn http_plain_proxy_attempts_allowed_unix_socket_proxy() { + let state = Arc::new(network_proxy_state_for_policy(NetworkProxySettings { + allow_unix_sockets: vec!["/tmp/test.sock".to_string()], + ..NetworkProxySettings::default() + })); + + let mut req = Request::builder() + .method(Method::GET) + .uri("http://example.com") + .header("x-unix-socket", "/tmp/test.sock") + .body(Body::empty()) + .expect("request should build"); + req.extensions_mut().insert(state); + + let response = http_plain_proxy(None, req).await.unwrap(); + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); + } + #[tokio::test] async fn http_connect_accept_denies_denylisted_host() { let policy = NetworkProxySettings { diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 6ed3233b3e..dadfe1d04a 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -43,6 +43,7 @@ pub use runtime::BlockedRequestObserver; pub use runtime::ConfigReloader; pub use runtime::ConfigState; pub use runtime::NetworkProxyState; +pub use state::NetworkProxyAuditMetadata; pub use state::NetworkProxyConstraintError; pub use state::NetworkProxyConstraints; pub use state::PartialNetworkConfig; diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index 6d8ec48d91..e759e75c0d 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -4,9 +4,21 @@ use crate::runtime::HostBlockReason; use crate::state::NetworkProxyState; use anyhow::Result; use async_trait::async_trait; +use chrono::SecondsFormat; +use chrono::Utc; use std::future::Future; use std::sync::Arc; +const AUDIT_TARGET: &str = "codex_otel.network_proxy"; +const POLICY_DECISION_EVENT_NAME: &str = "codex.network_proxy.policy_decision"; +const POLICY_SCOPE_DOMAIN: &str = "domain"; +const POLICY_SCOPE_NON_DOMAIN: &str = "non_domain"; +const POLICY_DECISION_ALLOW: &str = "allow"; +const POLICY_DECISION_DENY: &str = "deny"; +const POLICY_REASON_ALLOW: &str = "allow"; +const DEFAULT_METHOD: &str = "none"; +const DEFAULT_CLIENT_ADDRESS: &str = "unknown"; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NetworkProtocol { Http, @@ -154,6 +166,98 @@ impl NetworkDecision { } } +pub(crate) struct BlockDecisionAuditEventArgs<'a> { + pub source: NetworkDecisionSource, + pub reason: &'a str, + pub protocol: NetworkProtocol, + pub server_address: &'a str, + pub server_port: u16, + pub method: Option<&'a str>, + pub client_addr: Option<&'a str>, +} + +pub(crate) fn emit_block_decision_audit_event( + state: &NetworkProxyState, + args: BlockDecisionAuditEventArgs<'_>, +) { + emit_non_domain_policy_decision_audit_event(state, args, POLICY_DECISION_DENY); +} + +pub(crate) fn emit_allow_decision_audit_event( + state: &NetworkProxyState, + args: BlockDecisionAuditEventArgs<'_>, +) { + emit_non_domain_policy_decision_audit_event(state, args, POLICY_DECISION_ALLOW); +} + +fn emit_non_domain_policy_decision_audit_event( + state: &NetworkProxyState, + args: BlockDecisionAuditEventArgs<'_>, + decision: &'static str, +) { + emit_policy_audit_event( + state, + PolicyAuditEventArgs { + scope: POLICY_SCOPE_NON_DOMAIN, + decision, + source: args.source.as_str(), + reason: args.reason, + protocol: args.protocol, + server_address: args.server_address, + server_port: args.server_port, + method: args.method, + client_addr: args.client_addr, + policy_override: false, + }, + ); +} + +struct PolicyAuditEventArgs<'a> { + scope: &'static str, + decision: &'a str, + source: &'a str, + reason: &'a str, + protocol: NetworkProtocol, + server_address: &'a str, + server_port: u16, + method: Option<&'a str>, + client_addr: Option<&'a str>, + policy_override: bool, +} + +fn emit_policy_audit_event(state: &NetworkProxyState, args: PolicyAuditEventArgs<'_>) { + let metadata = state.audit_metadata(); + tracing::event!( + target: AUDIT_TARGET, + tracing::Level::INFO, + event.name = POLICY_DECISION_EVENT_NAME, + event.timestamp = %audit_timestamp(), + conversation.id = metadata.conversation_id.as_deref(), + app.version = metadata.app_version.as_deref(), + auth_mode = metadata.auth_mode.as_deref(), + originator = metadata.originator.as_deref(), + user.account_id = metadata.user_account_id.as_deref(), + user.email = metadata.user_email.as_deref(), + terminal.type = metadata.terminal_type.as_deref(), + model = metadata.model.as_deref(), + slug = metadata.slug.as_deref(), + network.policy.scope = args.scope, + network.policy.decision = args.decision, + network.policy.source = args.source, + network.policy.reason = args.reason, + network.transport.protocol = args.protocol.as_policy_protocol(), + server.address = args.server_address, + server.port = args.server_port, + http.request.method = args.method.unwrap_or(DEFAULT_METHOD), + client.address = args.client_addr.unwrap_or(DEFAULT_CLIENT_ADDRESS), + network.policy.override = args.policy_override, + ); +} + +fn audit_timestamp() -> String { + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) +} + /// Decide whether a network request should be allowed. /// /// If `command` or `exec_policy_hint` is provided, callers can map exec-policy @@ -187,23 +291,71 @@ pub(crate) async fn evaluate_host_policy( decider: Option<&Arc>, request: &NetworkPolicyRequest, ) -> Result { - match state.host_blocked(&request.host, request.port).await? { - HostBlockDecision::Allowed => Ok(NetworkDecision::Allow), + let host_decision = state.host_blocked(&request.host, request.port).await?; + let (decision, policy_override) = match host_decision { + HostBlockDecision::Allowed => (NetworkDecision::Allow, false), HostBlockDecision::Blocked(HostBlockReason::NotAllowed) => { if let Some(decider) = decider { - Ok(map_decider_decision(decider.decide(request.clone()).await)) + let decider_decision = map_decider_decision(decider.decide(request.clone()).await); + let policy_override = matches!(decider_decision, NetworkDecision::Allow); + (decider_decision, policy_override) } else { - Ok(NetworkDecision::deny_with_source( - HostBlockReason::NotAllowed.as_str(), - NetworkDecisionSource::BaselinePolicy, - )) + ( + NetworkDecision::deny_with_source( + HostBlockReason::NotAllowed.as_str(), + NetworkDecisionSource::BaselinePolicy, + ), + false, + ) } } - HostBlockDecision::Blocked(reason) => Ok(NetworkDecision::deny_with_source( - reason.as_str(), - NetworkDecisionSource::BaselinePolicy, - )), - } + HostBlockDecision::Blocked(reason) => ( + NetworkDecision::deny_with_source( + reason.as_str(), + NetworkDecisionSource::BaselinePolicy, + ), + false, + ), + }; + + let (policy_decision, source, reason) = match &decision { + NetworkDecision::Allow => ( + POLICY_DECISION_ALLOW, + if policy_override { + NetworkDecisionSource::Decider + } else { + NetworkDecisionSource::BaselinePolicy + }, + if policy_override { + HostBlockReason::NotAllowed.as_str() + } else { + POLICY_REASON_ALLOW + }, + ), + NetworkDecision::Deny { + reason, + source, + decision, + } => (decision.as_str(), *source, reason.as_str()), + }; + + emit_policy_audit_event( + state, + PolicyAuditEventArgs { + scope: POLICY_SCOPE_DOMAIN, + decision: policy_decision, + source: source.as_str(), + reason, + protocol: request.protocol, + server_address: request.host.as_str(), + server_port: request.port, + method: request.method.as_deref(), + client_addr: request.client_addr.as_deref(), + policy_override, + }, + ); + + Ok(decision) } fn map_decider_decision(decision: NetworkDecision) -> NetworkDecision { @@ -219,21 +371,244 @@ fn map_decider_decision(decision: NetworkDecision) -> NetworkDecision { } } +#[cfg(test)] +pub(crate) mod test_support { + pub(crate) const POLICY_DECISION_EVENT_NAME: &str = super::POLICY_DECISION_EVENT_NAME; + + use std::collections::BTreeMap; + use std::fmt; + use std::future::Future; + use std::sync::Arc; + use std::sync::Mutex; + use std::sync::atomic::AtomicU64; + use std::sync::atomic::Ordering; + use tracing::Event; + use tracing::Id; + use tracing::Metadata; + use tracing::Subscriber; + use tracing::field::Field; + use tracing::field::Visit; + use tracing::span::Attributes; + use tracing::span::Record; + use tracing::subscriber::Interest; + + #[derive(Clone, Debug, PartialEq, Eq)] + pub(crate) struct CapturedEvent { + pub target: String, + pub fields: BTreeMap, + } + + impl CapturedEvent { + pub fn field(&self, name: &str) -> Option<&str> { + self.fields.get(name).map(String::as_str) + } + } + + #[derive(Clone, Default)] + struct EventCollector { + events: Arc>>, + next_span_id: Arc, + } + + impl EventCollector { + fn events(&self) -> Vec { + self.events + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + } + + impl Subscriber for EventCollector { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest { + Interest::always() + } + + fn max_level_hint(&self) -> Option { + Some(tracing::level_filters::LevelFilter::TRACE) + } + + fn new_span(&self, _span: &Attributes<'_>) -> Id { + Id::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed) + 1) + } + + fn record(&self, _span: &Id, _values: &Record<'_>) {} + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &Event<'_>) { + let mut visitor = FieldVisitor::default(); + event.record(&mut visitor); + self.events + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(CapturedEvent { + target: event.metadata().target().to_string(), + fields: visitor.fields, + }); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} + } + + #[derive(Default)] + struct FieldVisitor { + fields: BTreeMap, + } + + impl FieldVisitor { + fn insert(&mut self, field: &Field, value: impl Into) { + self.fields.insert(field.name().to_string(), value.into()); + } + } + + impl Visit for FieldVisitor { + fn record_str(&mut self, field: &Field, value: &str) { + self.insert(field, value); + } + + fn record_bool(&mut self, field: &Field, value: bool) { + self.insert(field, value.to_string()); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.insert(field, value.to_string()); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.insert(field, value.to_string()); + } + + fn record_i128(&mut self, field: &Field, value: i128) { + self.insert(field, value.to_string()); + } + + fn record_u128(&mut self, field: &Field, value: u128) { + self.insert(field, value.to_string()); + } + + fn record_f64(&mut self, field: &Field, value: f64) { + self.insert(field, value.to_string()); + } + + fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) { + self.insert(field, value.to_string()); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + self.insert(field, format!("{value:?}")); + } + } + + pub(crate) async fn capture_events(f: F) -> (T, Vec) + where + F: FnOnce() -> Fut, + Fut: Future, + { + let collector = EventCollector::default(); + let _guard = tracing::subscriber::set_default(collector.clone()); + let output = f().await; + let events = collector.events(); + (output, events) + } + + pub(crate) fn find_event_by_name<'a>( + events: &'a [CapturedEvent], + event_name: &str, + ) -> Option<&'a CapturedEvent> { + events + .iter() + .find(|event| event.field("event.name") == Some(event_name)) + } +} + #[cfg(test)] mod tests { + use super::test_support::capture_events; + use super::test_support::find_event_by_name; use super::*; + use crate::config::NetworkMode; + use crate::config::NetworkProxyConfig; use crate::config::NetworkProxySettings; use crate::reasons::REASON_DENIED; + use crate::reasons::REASON_METHOD_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED; use crate::reasons::REASON_NOT_ALLOWED_LOCAL; + use crate::runtime::ConfigReloader; + use crate::runtime::ConfigState; + use crate::runtime::NetworkProxyAuditMetadata; + use crate::state::NetworkProxyConstraints; + use crate::state::build_config_state; use crate::state::network_proxy_state_for_policy; use pretty_assertions::assert_eq; use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; - #[tokio::test] - async fn evaluate_host_policy_invokes_decider_for_not_allowed() { + const LEGACY_DOMAIN_POLICY_DECISION_EVENT_NAME: &str = + "codex.network_proxy.domain_policy_decision"; + const LEGACY_BLOCK_DECISION_EVENT_NAME: &str = "codex.network_proxy.block_decision"; + + #[derive(Clone)] + struct StaticReloader { + state: ConfigState, + } + + #[async_trait] + impl ConfigReloader for StaticReloader { + async fn maybe_reload(&self) -> anyhow::Result> { + Ok(None) + } + + async fn reload_now(&self) -> anyhow::Result { + Ok(self.state.clone()) + } + + fn source_label(&self) -> String { + "static test reloader".to_string() + } + } + + fn state_with_metadata(metadata: NetworkProxyAuditMetadata) -> NetworkProxyState { + let network = NetworkProxySettings { + enabled: true, + mode: NetworkMode::Full, + ..NetworkProxySettings::default() + }; + let config = NetworkProxyConfig { network }; + let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let reloader = Arc::new(StaticReloader { + state: state.clone(), + }); + NetworkProxyState::with_reloader_and_audit_metadata(state, reloader, metadata) + } + + fn is_rfc3339_utc_millis(timestamp: &str) -> bool { + let bytes = timestamp.as_bytes(); + if bytes.len() != 24 { + return false; + } + bytes[4] == b'-' + && bytes[7] == b'-' + && bytes[10] == b'T' + && bytes[13] == b':' + && bytes[16] == b':' + && bytes[19] == b'.' + && bytes[23] == b'Z' + && bytes.iter().enumerate().all(|(idx, value)| match idx { + 4 | 7 | 10 | 13 | 16 | 19 | 23 => true, + _ => value.is_ascii_digit(), + }) + } + + #[tokio::test(flavor = "current_thread")] + async fn evaluate_host_policy_emits_domain_event_for_decider_allow_override() { let state = network_proxy_state_for_policy(NetworkProxySettings::default()); let calls = Arc::new(AtomicUsize::new(0)); let decider: Arc = Arc::new({ @@ -251,47 +626,75 @@ mod tests { host: "example.com".to_string(), port: 80, client_addr: None, - method: Some("GET".to_string()), + method: None, command: None, exec_policy_hint: None, }); - let decision = evaluate_host_policy(&state, Some(&decider), &request) - .await - .unwrap(); + let (decision, events) = capture_events(|| async { + evaluate_host_policy(&state, Some(&decider), &request) + .await + .unwrap() + }) + .await; assert_eq!(decision, NetworkDecision::Allow); assert_eq!(calls.load(Ordering::SeqCst), 1); + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision audit event"); + assert_eq!(event.target, AUDIT_TARGET); + assert!(event.target.starts_with("codex_otel.")); + assert_eq!( + event.field("network.policy.scope"), + Some(POLICY_SCOPE_DOMAIN) + ); + assert_eq!(event.field("network.policy.decision"), Some("allow")); + assert_eq!(event.field("network.policy.source"), Some("decider")); + assert_eq!( + event.field("network.policy.reason"), + Some(REASON_NOT_ALLOWED) + ); + assert_eq!(event.field("network.transport.protocol"), Some("http")); + assert_eq!(event.field("server.address"), Some("example.com")); + assert_eq!(event.field("server.port"), Some("80")); + assert_eq!(event.field("http.request.method"), Some(DEFAULT_METHOD)); + assert_eq!(event.field("client.address"), Some(DEFAULT_CLIENT_ADDRESS)); + assert_eq!(event.field("network.policy.override"), Some("true")); + let timestamp = event + .field("event.timestamp") + .expect("event timestamp should be present"); + assert!(is_rfc3339_utc_millis(timestamp)); + assert_eq!( + find_event_by_name(&events, LEGACY_DOMAIN_POLICY_DECISION_EVENT_NAME), + None + ); + assert_eq!( + find_event_by_name(&events, LEGACY_BLOCK_DECISION_EVENT_NAME), + None + ); } - #[tokio::test] - async fn evaluate_host_policy_skips_decider_for_denied() { + #[tokio::test(flavor = "current_thread")] + async fn evaluate_host_policy_emits_domain_event_for_baseline_deny() { let state = network_proxy_state_for_policy(NetworkProxySettings { allowed_domains: vec!["example.com".to_string()], denied_domains: vec!["blocked.com".to_string()], ..NetworkProxySettings::default() }); - let calls = Arc::new(AtomicUsize::new(0)); - let decider: Arc = Arc::new({ - let calls = calls.clone(); - move |_req| { - calls.fetch_add(1, Ordering::SeqCst); - async { NetworkDecision::Allow } - } - }); - let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { protocol: NetworkProtocol::Http, host: "blocked.com".to_string(), port: 80, - client_addr: None, + client_addr: Some("127.0.0.1:1234".to_string()), method: Some("GET".to_string()), command: None, exec_policy_hint: None, }); - let decision = evaluate_host_policy(&state, Some(&decider), &request) - .await - .unwrap(); + let (decision, events) = capture_events(|| async { + evaluate_host_policy(&state, None, &request).await.unwrap() + }) + .await; assert_eq!( decision, NetworkDecision::Deny { @@ -300,25 +703,158 @@ mod tests { decision: NetworkPolicyDecision::Deny, } ); - assert_eq!(calls.load(Ordering::SeqCst), 0); + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision audit event"); + assert_eq!(event.field("network.policy.decision"), Some("deny")); + assert_eq!( + event.field("network.policy.source"), + Some("baseline_policy") + ); + assert_eq!(event.field("network.policy.reason"), Some(REASON_DENIED)); + assert_eq!(event.field("network.policy.override"), Some("false")); + assert_eq!(event.field("http.request.method"), Some("GET")); + assert_eq!(event.field("client.address"), Some("127.0.0.1:1234")); } - #[tokio::test] - async fn evaluate_host_policy_skips_decider_for_not_allowed_local() { + #[tokio::test(flavor = "current_thread")] + async fn evaluate_host_policy_emits_domain_event_for_decider_ask() { + let state = network_proxy_state_for_policy(NetworkProxySettings::default()); + let decider: Arc = + Arc::new(|_req| async { NetworkDecision::ask(REASON_NOT_ALLOWED) }); + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Http, + host: "example.com".to_string(), + port: 80, + client_addr: None, + method: Some("GET".to_string()), + command: None, + exec_policy_hint: None, + }); + + let (decision, events) = capture_events(|| async { + evaluate_host_policy(&state, Some(&decider), &request) + .await + .unwrap() + }) + .await; + assert_eq!( + decision, + NetworkDecision::Deny { + reason: REASON_NOT_ALLOWED.to_string(), + source: NetworkDecisionSource::Decider, + decision: NetworkPolicyDecision::Ask, + } + ); + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision audit event"); + assert_eq!(event.field("network.policy.decision"), Some("ask")); + assert_eq!(event.field("network.policy.source"), Some("decider")); + assert_eq!( + event.field("network.policy.reason"), + Some(REASON_NOT_ALLOWED) + ); + assert_eq!(event.field("network.policy.override"), Some("false")); + } + + #[tokio::test(flavor = "current_thread")] + async fn evaluate_host_policy_emits_metadata_fields() { + let metadata = NetworkProxyAuditMetadata { + conversation_id: Some("conversation-1".to_string()), + app_version: Some("1.2.3".to_string()), + user_account_id: Some("acct-1".to_string()), + auth_mode: Some("Chatgpt".to_string()), + originator: Some("codex_cli_rs".to_string()), + user_email: Some("test@example.com".to_string()), + terminal_type: Some("iTerm.app/3.6.5".to_string()), + model: Some("gpt-5.3-codex".to_string()), + slug: Some("gpt-5.3-codex".to_string()), + }; + let state = state_with_metadata(metadata); + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Http, + host: "example.com".to_string(), + port: 80, + client_addr: None, + method: Some("GET".to_string()), + command: None, + exec_policy_hint: None, + }); + + let (_decision, events) = capture_events(|| async { + evaluate_host_policy(&state, None, &request).await.unwrap() + }) + .await; + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision audit event"); + assert_eq!(event.field("conversation.id"), Some("conversation-1")); + assert_eq!(event.field("app.version"), Some("1.2.3")); + assert_eq!(event.field("auth_mode"), Some("Chatgpt")); + assert_eq!(event.field("originator"), Some("codex_cli_rs")); + assert_eq!(event.field("user.account_id"), Some("acct-1")); + assert_eq!(event.field("user.email"), Some("test@example.com")); + assert_eq!(event.field("terminal.type"), Some("iTerm.app/3.6.5")); + assert_eq!(event.field("model"), Some("gpt-5.3-codex")); + assert_eq!(event.field("slug"), Some("gpt-5.3-codex")); + } + + #[tokio::test(flavor = "current_thread")] + async fn emit_block_decision_audit_event_emits_non_domain_event() { + let state = network_proxy_state_for_policy(NetworkProxySettings::default()); + + let (_, events) = capture_events(|| async { + emit_block_decision_audit_event( + &state, + BlockDecisionAuditEventArgs { + source: NetworkDecisionSource::ModeGuard, + reason: REASON_METHOD_NOT_ALLOWED, + protocol: NetworkProtocol::Http, + server_address: "unix-socket", + server_port: 0, + method: Some("POST"), + client_addr: None, + }, + ); + }) + .await; + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision audit event"); + assert_eq!(event.target, AUDIT_TARGET); + assert_eq!( + event.field("network.policy.scope"), + Some(POLICY_SCOPE_NON_DOMAIN) + ); + assert_eq!( + event.field("network.policy.decision"), + Some(POLICY_DECISION_DENY) + ); + assert_eq!(event.field("network.policy.source"), Some("mode_guard")); + assert_eq!( + event.field("network.policy.reason"), + Some(REASON_METHOD_NOT_ALLOWED) + ); + assert_eq!(event.field("network.transport.protocol"), Some("http")); + assert_eq!(event.field("server.address"), Some("unix-socket")); + assert_eq!(event.field("server.port"), Some("0")); + assert_eq!(event.field("http.request.method"), Some("POST")); + assert_eq!(event.field("client.address"), Some(DEFAULT_CLIENT_ADDRESS)); + assert_eq!(event.field("network.policy.override"), Some("false")); + assert_eq!( + find_event_by_name(&events, LEGACY_BLOCK_DECISION_EVENT_NAME), + None + ); + } + + #[tokio::test(flavor = "current_thread")] + async fn evaluate_host_policy_still_denies_not_allowed_local_without_decider_override() { let state = network_proxy_state_for_policy(NetworkProxySettings { allowed_domains: vec!["example.com".to_string()], allow_local_binding: false, ..NetworkProxySettings::default() }); - let calls = Arc::new(AtomicUsize::new(0)); - let decider: Arc = Arc::new({ - let calls = calls.clone(); - move |_req| { - calls.fetch_add(1, Ordering::SeqCst); - async { NetworkDecision::Allow } - } - }); - let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { protocol: NetworkProtocol::Http, host: "127.0.0.1".to_string(), @@ -329,9 +865,7 @@ mod tests { exec_policy_hint: None, }); - let decision = evaluate_host_policy(&state, Some(&decider), &request) - .await - .unwrap(); + let decision = evaluate_host_policy(&state, None, &request).await.unwrap(); assert_eq!( decision, NetworkDecision::Deny { @@ -340,7 +874,6 @@ mod tests { decision: NetworkPolicyDecision::Deny, } ); - assert_eq!(calls.load(Ordering::SeqCst), 0); } #[test] diff --git a/codex-rs/network-proxy/src/reasons.rs b/codex-rs/network-proxy/src/reasons.rs index cb31db36a4..b844eadf3d 100644 --- a/codex-rs/network-proxy/src/reasons.rs +++ b/codex-rs/network-proxy/src/reasons.rs @@ -5,3 +5,4 @@ pub(crate) const REASON_NOT_ALLOWED: &str = "not_allowed"; pub(crate) const REASON_NOT_ALLOWED_LOCAL: &str = "not_allowed_local"; pub(crate) const REASON_POLICY_DENIED: &str = "policy_denied"; pub(crate) const REASON_PROXY_DISABLED: &str = "proxy_disabled"; +pub(crate) const REASON_UNIX_SOCKET_UNSUPPORTED: &str = "unix_socket_unsupported"; diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index b0909c2680..46baa85bfd 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -38,6 +38,19 @@ const MAX_BLOCKED_EVENTS: usize = 200; const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2); const NETWORK_POLICY_VIOLATION_PREFIX: &str = "CODEX_NETWORK_POLICY_VIOLATION"; +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NetworkProxyAuditMetadata { + pub conversation_id: Option, + pub app_version: Option, + pub user_account_id: Option, + pub auth_mode: Option, + pub originator: Option, + pub user_email: Option, + pub terminal_type: Option, + pub model: Option, + pub slug: Option, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HostBlockReason { Denied, @@ -187,6 +200,7 @@ pub struct NetworkProxyState { state: Arc>, reloader: Arc, blocked_request_observer: Arc>>>, + audit_metadata: NetworkProxyAuditMetadata, } impl std::fmt::Debug for NetworkProxyState { @@ -203,24 +217,57 @@ impl Clone for NetworkProxyState { state: self.state.clone(), reloader: self.reloader.clone(), blocked_request_observer: self.blocked_request_observer.clone(), + audit_metadata: self.audit_metadata.clone(), } } } impl NetworkProxyState { pub fn with_reloader(state: ConfigState, reloader: Arc) -> Self { - Self::with_reloader_and_blocked_observer(state, reloader, None) + Self::with_reloader_and_audit_metadata( + state, + reloader, + NetworkProxyAuditMetadata::default(), + ) } pub fn with_reloader_and_blocked_observer( state: ConfigState, reloader: Arc, blocked_request_observer: Option>, + ) -> Self { + Self::with_reloader_and_audit_metadata_and_blocked_observer( + state, + reloader, + NetworkProxyAuditMetadata::default(), + blocked_request_observer, + ) + } + + pub fn with_reloader_and_audit_metadata( + state: ConfigState, + reloader: Arc, + audit_metadata: NetworkProxyAuditMetadata, + ) -> Self { + Self::with_reloader_and_audit_metadata_and_blocked_observer( + state, + reloader, + audit_metadata, + None, + ) + } + + pub fn with_reloader_and_audit_metadata_and_blocked_observer( + state: ConfigState, + reloader: Arc, + audit_metadata: NetworkProxyAuditMetadata, + blocked_request_observer: Option>, ) -> Self { Self { state: Arc::new(RwLock::new(state)), reloader, blocked_request_observer: Arc::new(RwLock::new(blocked_request_observer)), + audit_metadata, } } @@ -232,6 +279,10 @@ impl NetworkProxyState { *observer = blocked_request_observer; } + pub fn audit_metadata(&self) -> &NetworkProxyAuditMetadata { + &self.audit_metadata + } + pub async fn current_cfg(&self) -> Result { // Callers treat `NetworkProxyState` as a live view of policy. We reload-on-demand so edits to // `config.toml` (including Codex-managed writes) take effect without a restart. diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs index 409bd82e37..30ec4d2903 100644 --- a/codex-rs/network-proxy/src/socks5.rs +++ b/codex-rs/network-proxy/src/socks5.rs @@ -1,4 +1,5 @@ use crate::config::NetworkMode; +use crate::network_policy::BlockDecisionAuditEventArgs; use crate::network_policy::NetworkDecision; use crate::network_policy::NetworkDecisionSource; use crate::network_policy::NetworkPolicyDecider; @@ -6,6 +7,7 @@ use crate::network_policy::NetworkPolicyDecision; use crate::network_policy::NetworkPolicyRequest; use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; +use crate::network_policy::emit_block_decision_audit_event; use crate::network_policy::evaluate_host_policy; use crate::policy::normalize_host; use crate::reasons::REASON_METHOD_NOT_ALLOWED; @@ -152,6 +154,15 @@ async fn handle_socks5_tcp( match app_state.enabled().await { Ok(true) => {} Ok(false) => { + emit_socks_block_decision_audit_event( + &app_state, + NetworkDecisionSource::ProxyState, + REASON_PROXY_DISABLED, + NetworkProtocol::Socks5Tcp, + host.as_str(), + port, + client.as_deref(), + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_PROXY_DISABLED, @@ -185,6 +196,15 @@ async fn handle_socks5_tcp( match app_state.network_mode().await { Ok(NetworkMode::Limited) => { + emit_socks_block_decision_audit_event( + &app_state, + NetworkDecisionSource::ModeGuard, + REASON_METHOD_NOT_ALLOWED, + NetworkProtocol::Socks5Tcp, + host.as_str(), + port, + client.as_deref(), + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_METHOD_NOT_ALLOWED, @@ -298,6 +318,15 @@ async fn inspect_socks5_udp( match state.enabled().await { Ok(true) => {} Ok(false) => { + emit_socks_block_decision_audit_event( + &state, + NetworkDecisionSource::ProxyState, + REASON_PROXY_DISABLED, + NetworkProtocol::Socks5Udp, + host.as_str(), + port, + client.as_deref(), + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_PROXY_DISABLED, @@ -331,6 +360,15 @@ async fn inspect_socks5_udp( match state.network_mode().await { Ok(NetworkMode::Limited) => { + emit_socks_block_decision_audit_event( + &state, + NetworkDecisionSource::ModeGuard, + REASON_METHOD_NOT_ALLOWED, + NetworkProtocol::Socks5Udp, + host.as_str(), + port, + client.as_deref(), + ); let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Deny, reason: REASON_METHOD_NOT_ALLOWED, @@ -413,9 +451,159 @@ async fn inspect_socks5_udp( } } +fn emit_socks_block_decision_audit_event( + state: &NetworkProxyState, + source: NetworkDecisionSource, + reason: &str, + protocol: NetworkProtocol, + host: &str, + port: u16, + client_addr: Option<&str>, +) { + emit_block_decision_audit_event( + state, + BlockDecisionAuditEventArgs { + source, + reason, + protocol, + server_address: host, + server_port: port, + method: None, + client_addr, + }, + ); +} + fn policy_denied_error(reason: &str, details: &PolicyDecisionDetails<'_>) -> io::Error { io::Error::new( io::ErrorKind::PermissionDenied, blocked_message_with_policy(reason, details), ) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::NetworkMode; + use crate::config::NetworkProxyConfig; + use crate::config::NetworkProxySettings; + use crate::network_policy::test_support::POLICY_DECISION_EVENT_NAME; + use crate::network_policy::test_support::capture_events; + use crate::network_policy::test_support::find_event_by_name; + use crate::runtime::ConfigReloader; + use crate::runtime::ConfigState; + use crate::state::NetworkProxyConstraints; + use crate::state::build_config_state; + use async_trait::async_trait; + use pretty_assertions::assert_eq; + use rama_core::extensions::Extensions; + use rama_core::extensions::ExtensionsMut; + use rama_net::address::HostWithPort; + use rama_net::address::SocketAddress; + use rama_socks5::server::udp::RelayDirection; + use std::net::IpAddr; + use std::net::Ipv4Addr; + use std::sync::Arc; + + #[derive(Clone)] + struct StaticReloader { + state: ConfigState, + } + + #[async_trait] + impl ConfigReloader for StaticReloader { + async fn maybe_reload(&self) -> anyhow::Result> { + Ok(None) + } + + async fn reload_now(&self) -> anyhow::Result { + Ok(self.state.clone()) + } + + fn source_label(&self) -> String { + "static test reloader".to_string() + } + } + + fn state_for_settings(network: NetworkProxySettings) -> Arc { + let config = NetworkProxyConfig { network }; + let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap(); + let reloader = Arc::new(StaticReloader { + state: state.clone(), + }); + Arc::new(NetworkProxyState::with_reloader(state, reloader)) + } + + #[tokio::test(flavor = "current_thread")] + async fn handle_socks5_tcp_emits_block_decision_for_proxy_disabled() { + let state = state_for_settings(NetworkProxySettings { + enabled: false, + mode: NetworkMode::Full, + ..NetworkProxySettings::default() + }); + let mut request = + TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority")); + request.extensions_mut().insert(state.clone()); + + let (result, events) = capture_events(|| async { + handle_socks5_tcp(request, TcpConnector::default(), None).await + }) + .await; + assert!(result.is_err(), "proxy-disabled request should be denied"); + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision event"); + assert_eq!(event.field("network.policy.scope"), Some("non_domain")); + assert_eq!(event.field("network.policy.decision"), Some("deny")); + assert_eq!(event.field("network.policy.source"), Some("proxy_state")); + assert_eq!( + event.field("network.policy.reason"), + Some(REASON_PROXY_DISABLED) + ); + assert_eq!( + event.field("network.transport.protocol"), + Some("socks5_tcp") + ); + assert_eq!(event.field("server.address"), Some("example.com")); + assert_eq!(event.field("server.port"), Some("443")); + assert_eq!(event.field("http.request.method"), Some("none")); + assert_eq!(event.field("client.address"), Some("unknown")); + } + + #[tokio::test(flavor = "current_thread")] + async fn inspect_socks5_udp_emits_block_decision_for_mode_guard_deny() { + let state = state_for_settings(NetworkProxySettings { + enabled: true, + mode: NetworkMode::Limited, + ..NetworkProxySettings::default() + }); + let request = RelayRequest { + direction: RelayDirection::South, + server_address: SocketAddress::new(IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), 53), + payload: Default::default(), + extensions: Extensions::new(), + }; + + let (result, events) = + capture_events(|| async { inspect_socks5_udp(request, state, None).await }).await; + assert!(result.is_err(), "limited-mode UDP request should be denied"); + + let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME) + .expect("expected policy decision event"); + assert_eq!(event.field("network.policy.scope"), Some("non_domain")); + assert_eq!(event.field("network.policy.decision"), Some("deny")); + assert_eq!(event.field("network.policy.source"), Some("mode_guard")); + assert_eq!( + event.field("network.policy.reason"), + Some(REASON_METHOD_NOT_ALLOWED) + ); + assert_eq!( + event.field("network.transport.protocol"), + Some("socks5_udp") + ); + assert_eq!(event.field("server.address"), Some("93.184.216.34")); + assert_eq!(event.field("server.port"), Some("53")); + assert_eq!(event.field("http.request.method"), Some("none")); + assert_eq!(event.field("client.address"), Some("unknown")); + } +} diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index f2f49984b1..906246ed45 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -10,6 +10,7 @@ use std::sync::Arc; pub use crate::runtime::BlockedRequest; pub use crate::runtime::BlockedRequestArgs; +pub use crate::runtime::NetworkProxyAuditMetadata; pub use crate::runtime::NetworkProxyState; #[cfg(test)] pub(crate) use crate::runtime::network_proxy_state_for_policy; diff --git a/codex-rs/node-version.txt b/codex-rs/node-version.txt index 32f8c50de0..85e502778f 100644 --- a/codex-rs/node-version.txt +++ b/codex-rs/node-version.txt @@ -1 +1 @@ -24.13.1 +22.22.0 diff --git a/codex-rs/otel/Cargo.toml b/codex-rs/otel/Cargo.toml index 6e6321d2e5..0fa14ff541 100644 --- a/codex-rs/otel/Cargo.toml +++ b/codex-rs/otel/Cargo.toml @@ -26,6 +26,7 @@ codex-utils-string = { workspace = true } codex-api = { workspace = true } codex-protocol = { workspace = true } eventsource-stream = { workspace = true } +gethostname = { workspace = true } opentelemetry = { workspace = true, features = ["logs", "metrics", "trace"] } opentelemetry-appender-tracing = { workspace = true } opentelemetry-otlp = { workspace = true, features = [ diff --git a/codex-rs/otel/src/lib.rs b/codex-rs/otel/src/lib.rs index 8a6d1f04ff..5aaee5f5a9 100644 --- a/codex-rs/otel/src/lib.rs +++ b/codex-rs/otel/src/lib.rs @@ -45,6 +45,7 @@ pub struct OtelEventMetadata { pub(crate) account_id: Option, pub(crate) account_email: Option, pub(crate) originator: String, + pub(crate) service_name: Option, pub(crate) session_source: String, pub(crate) model: String, pub(crate) slug: String, @@ -67,6 +68,11 @@ impl OtelManager { self } + pub fn with_metrics_service_name(mut self, service_name: &str) -> Self { + self.metadata.service_name = Some(sanitize_metric_tag_value(service_name)); + self + } + pub fn with_metrics(mut self, metrics: MetricsClient) -> Self { self.metrics = Some(metrics); self.metrics_use_metadata_tags = true; @@ -197,7 +203,7 @@ impl OtelManager { if !self.metrics_use_metadata_tags { return Ok(Vec::new()); } - let mut tags = Vec::with_capacity(6); + let mut tags = Vec::with_capacity(7); Self::push_metadata_tag(&mut tags, "auth_mode", self.metadata.auth_mode.as_deref())?; Self::push_metadata_tag( &mut tags, @@ -209,6 +215,11 @@ impl OtelManager { "originator", Some(self.metadata.originator.as_str()), )?; + Self::push_metadata_tag( + &mut tags, + "service_name", + self.metadata.service_name.as_deref(), + )?; Self::push_metadata_tag(&mut tags, "model", Some(self.metadata.model.as_str()))?; Self::push_metadata_tag(&mut tags, "app.version", Some(self.metadata.app_version))?; Ok(tags) diff --git a/codex-rs/otel/src/otel_provider.rs b/codex-rs/otel/src/otel_provider.rs index b1ea099fa2..6f2dc5a09d 100644 --- a/codex-rs/otel/src/otel_provider.rs +++ b/codex-rs/otel/src/otel_provider.rs @@ -3,6 +3,7 @@ use crate::config::OtelHttpProtocol; use crate::config::OtelSettings; use crate::metrics::MetricsClient; use crate::metrics::MetricsConfig; +use gethostname::gethostname; use opentelemetry::Context; use opentelemetry::KeyValue; use opentelemetry::context::ContextGuard; @@ -40,6 +41,7 @@ use tracing_subscriber::Layer; use tracing_subscriber::registry::LookupSpan; const ENV_ATTRIBUTE: &str = "env"; +const HOST_NAME_ATTRIBUTE: &str = "host.name"; const TRACEPARENT_ENV_VAR: &str = "TRACEPARENT"; const TRACESTATE_ENV_VAR: &str = "TRACESTATE"; static TRACEPARENT_CONTEXT: OnceLock> = OnceLock::new(); @@ -223,16 +225,37 @@ fn extract_traceparent_context(traceparent: String, tracestate: Option) fn make_resource(settings: &OtelSettings) -> Resource { Resource::builder() .with_service_name(settings.service_name.clone()) - .with_attributes(vec![ - KeyValue::new( - semconv::attribute::SERVICE_VERSION, - settings.service_version.clone(), - ), - KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()), - ]) + .with_attributes(resource_attributes( + settings, + detected_host_name().as_deref(), + )) .build() } +fn resource_attributes(settings: &OtelSettings, host_name: Option<&str>) -> Vec { + let mut attributes = vec![ + KeyValue::new( + semconv::attribute::SERVICE_VERSION, + settings.service_version.clone(), + ), + KeyValue::new(ENV_ATTRIBUTE, settings.environment.clone()), + ]; + if let Some(host_name) = host_name.and_then(normalize_host_name) { + attributes.push(KeyValue::new(HOST_NAME_ATTRIBUTE, host_name)); + } + attributes +} + +fn detected_host_name() -> Option { + let host_name = gethostname(); + normalize_host_name(host_name.to_string_lossy().as_ref()) +} + +fn normalize_host_name(host_name: &str) -> Option { + let host_name = host_name.trim(); + (!host_name.is_empty()).then(|| host_name.to_owned()) +} + fn build_logger( resource: &Resource, exporter: &OtelExporter, @@ -377,6 +400,8 @@ mod tests { use opentelemetry::trace::SpanId; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; + use pretty_assertions::assert_eq; + use std::path::PathBuf; #[test] fn parses_valid_traceparent() { @@ -398,4 +423,46 @@ mod tests { fn invalid_traceparent_returns_none() { assert!(extract_traceparent_context("not-a-traceparent".to_string(), None).is_none()); } + + #[test] + fn resource_attributes_include_host_name_when_present() { + let attrs = resource_attributes(&test_otel_settings(), Some("opentelemetry-test")); + + let host_name = attrs + .iter() + .find(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + .map(|kv| kv.value.as_str().to_string()); + + assert_eq!(host_name, Some("opentelemetry-test".to_string())); + } + + #[test] + fn resource_attributes_omit_host_name_when_missing_or_empty() { + let missing = resource_attributes(&test_otel_settings(), None); + let empty = resource_attributes(&test_otel_settings(), Some(" ")); + + assert!( + !missing + .iter() + .any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + ); + assert!( + !empty + .iter() + .any(|kv| kv.key.as_str() == HOST_NAME_ATTRIBUTE) + ); + } + + fn test_otel_settings() -> OtelSettings { + OtelSettings { + environment: "test".to_string(), + service_name: "codex-test".to_string(), + service_version: "0.0.0".to_string(), + codex_home: PathBuf::from("."), + exporter: OtelExporter::None, + trace_exporter: OtelExporter::None, + metrics_exporter: OtelExporter::None, + runtime_metrics: false, + } + } } diff --git a/codex-rs/otel/src/traces/otel_manager.rs b/codex-rs/otel/src/traces/otel_manager.rs index 9331801f9d..32b9fba860 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -79,6 +79,7 @@ impl OtelManager { account_id, account_email, originator: sanitize_metric_tag_value(originator.as_str()), + service_name: None, session_source: session_source.to_string(), model: model.to_owned(), slug: slug.to_owned(), diff --git a/codex-rs/otel/tests/suite/manager_metrics.rs b/codex-rs/otel/tests/suite/manager_metrics.rs index 1012a1b36e..53a9cc89d6 100644 --- a/codex-rs/otel/tests/suite/manager_metrics.rs +++ b/codex-rs/otel/tests/suite/manager_metrics.rs @@ -109,3 +109,47 @@ fn manager_allows_disabling_metadata_tags() -> Result<()> { Ok(()) } + +#[test] +fn manager_attaches_optional_service_name_tag() -> Result<()> { + let (metrics, exporter) = build_metrics_with_defaults(&[])?; + let manager = OtelManager::new( + ThreadId::new(), + "gpt-5.1", + "gpt-5.1", + None, + None, + None, + "test_originator".to_string(), + false, + "tty".to_string(), + SessionSource::Cli, + ) + .with_metrics_service_name("my_app_server_client") + .with_metrics(metrics); + + manager.counter("codex.session_started", 1, &[]); + manager.shutdown_metrics()?; + + let resource_metrics = latest_metrics(&exporter); + let metric = + find_metric(&resource_metrics, "codex.session_started").expect("counter metric missing"); + let attrs = match metric.data() { + AggregatedMetrics::U64(data) => match data { + MetricData::Sum(sum) => { + let points: Vec<_> = sum.data_points().collect(); + assert_eq!(points.len(), 1); + attributes_to_map(points[0].attributes()) + } + _ => panic!("unexpected counter aggregation"), + }, + _ => panic!("unexpected counter data type"), + }; + + assert_eq!( + attrs.get("service_name"), + Some(&"my_app_server_client".to_string()) + ); + + Ok(()) +} diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index e0f0c06254..12bd65ec59 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -2,14 +2,30 @@ use std::collections::HashMap; use std::path::PathBuf; use crate::mcp::RequestId; -use crate::models::AdditionalPermissions; +use crate::models::MacOsSeatbeltProfileExtensions; +use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; +use crate::protocol::ReviewDecision; +use crate::protocol::SandboxPolicy; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Permissions { + pub sandbox_policy: SandboxPolicy, + pub macos_seatbelt_profile_extensions: Option, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EscalationPermissions { + PermissionProfile(PermissionProfile), + Permissions(Permissions), +} + /// Proposed execpolicy change to allow commands starting with this prefix. /// /// The `command` tokens form the prefix that would be added as an execpolicy @@ -106,7 +122,14 @@ pub struct ExecApprovalRequestEvent { /// Optional additional filesystem permissions requested for this command. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, + /// Ordered list of decisions the client may present for this prompt. + /// + /// When absent, clients should derive the legacy default set from the + /// other fields on this request. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub available_decisions: Option>, pub parsed_cmd: Vec, } @@ -116,6 +139,55 @@ impl ExecApprovalRequestEvent { .clone() .unwrap_or_else(|| self.call_id.clone()) } + + pub fn effective_available_decisions(&self) -> Vec { + // available_decisions is a new field that may not be populated by older + // senders, so we fall back to the legacy logic if it's not present. + match &self.available_decisions { + Some(decisions) => decisions.clone(), + None => Self::default_available_decisions( + self.network_approval_context.as_ref(), + self.proposed_execpolicy_amendment.as_ref(), + self.proposed_network_policy_amendments.as_deref(), + self.additional_permissions.as_ref(), + ), + } + } + + pub fn default_available_decisions( + network_approval_context: Option<&NetworkApprovalContext>, + proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>, + proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>, + additional_permissions: Option<&PermissionProfile>, + ) -> Vec { + if network_approval_context.is_some() { + let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession]; + if let Some(amendment) = proposed_network_policy_amendments.and_then(|amendments| { + amendments + .iter() + .find(|amendment| amendment.action == NetworkPolicyRuleAction::Allow) + }) { + decisions.push(ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: amendment.clone(), + }); + } + decisions.push(ReviewDecision::Abort); + return decisions; + } + + if additional_permissions.is_some() { + return vec![ReviewDecision::Approved, ReviewDecision::Abort]; + } + + let mut decisions = vec![ReviewDecision::Approved]; + if let Some(prefix) = proposed_execpolicy_amendment { + decisions.push(ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: prefix.clone(), + }); + } + decisions.push(ReviewDecision::Abort); + decisions + } } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index c942a717d7..5841b1187e 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -15,5 +15,4 @@ pub mod parse_command; pub mod plan_tool; pub mod protocol; pub mod request_user_input; -pub mod skill_approval; pub mod user_input; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index c9fea3a295..cf06636ad8 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::path::Path; -use std::path::PathBuf; use codex_utils_image::load_and_resize_to_fit; use serde::Deserialize; @@ -20,6 +19,7 @@ use crate::protocol::WritableRoot; use crate::user_input::UserInput; use codex_execpolicy::Policy; use codex_git::GhostCommit; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_image::error::ImageProcessingError; use schemars::JsonSchema; @@ -53,16 +53,94 @@ impl SandboxPermissions { } #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] -pub struct AdditionalPermissions { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub fs_read: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub fs_write: Vec, +pub struct FileSystemPermissions { + pub read: Option>, + pub write: Option>, } -impl AdditionalPermissions { +impl FileSystemPermissions { pub fn is_empty(&self) -> bool { - self.fs_read.is_empty() && self.fs_write.is_empty() + self.read.is_none() && self.write.is_none() + } +} + +#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +pub struct MacOsPermissions { + pub preferences: Option, + pub automations: Option, + pub accessibility: Option, + pub calendar: Option, +} + +impl MacOsPermissions { + pub fn is_empty(&self) -> bool { + self.preferences.is_none() + && self.automations.is_none() + && self.accessibility.is_none() + && self.calendar.is_none() + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(untagged)] +pub enum MacOsPreferencesValue { + Bool(bool), + Mode(String), +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(untagged)] +pub enum MacOsAutomationValue { + Bool(bool), + BundleIds(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum MacOsPreferencesPermission { + // IMPORTANT: ReadOnly needs to be the default because it's the + // security-sensitive default and keeps cf prefs working. + #[default] + ReadOnly, + ReadWrite, + None, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum MacOsAutomationPermission { + #[default] + None, + All, + BundleIds(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MacOsSeatbeltProfileExtensions { + pub macos_preferences: MacOsPreferencesPermission, + pub macos_automation: MacOsAutomationPermission, + pub macos_accessibility: bool, + pub macos_calendar: bool, +} + +#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +pub struct PermissionProfile { + pub network: Option, + pub file_system: Option, + pub macos: Option, +} + +impl PermissionProfile { + pub fn is_empty(&self) -> bool { + self.network.is_none() + && self + .file_system + .as_ref() + .map(FileSystemPermissions::is_empty) + .unwrap_or(true) + && self + .macos + .as_ref() + .map(MacOsPermissions::is_empty) + .unwrap_or(true) } } @@ -83,7 +161,7 @@ pub enum ResponseInputItem { }, CustomToolCallOutput { call_id: String, - output: String, + output: FunctionCallOutputPayload, }, } @@ -183,9 +261,12 @@ pub enum ResponseItem { name: String, input: String, }, + // `custom_tool_call_output.output` uses the same wire encoding as + // `function_call_output.output` so freeform tools can return either plain + // text or structured content items. CustomToolCallOutput { call_id: String, - output: String, + output: FunctionCallOutputPayload, }, // Emitted by the Responses API when the agent triggers a web search. // Example payload (from SSE `response.output_item.done`): @@ -800,7 +881,7 @@ pub struct ShellToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -826,7 +907,7 @@ pub struct ShellCommandToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -1460,6 +1541,26 @@ mod tests { Ok(()) } + #[test] + fn serializes_custom_tool_image_outputs_as_array() -> Result<()> { + let item = ResponseInputItem::CustomToolCallOutput { + call_id: "call1".into(), + output: FunctionCallOutputPayload::from_content_items(vec![ + FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,BASE64".into(), + }, + ]), + }; + + let json = serde_json::to_string(&item)?; + let v: serde_json::Value = serde_json::from_str(&json)?; + + let output = v.get("output").expect("output field"); + assert!(output.is_array(), "expected array output"); + + Ok(()) + } + #[test] fn preserves_existing_image_data_urls() -> Result<()> { let call_tool_result = CallToolResult { diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 8d01e10d0b..b63f137442 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -15,6 +15,7 @@ use tracing::warn; use ts_rs::TS; use crate::config_types::Personality; +use crate::config_types::ReasoningSummary; use crate::config_types::Verbosity; const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}"; @@ -98,6 +99,11 @@ pub struct ModelUpgrade { pub migration_markdown: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +pub struct ModelAvailabilityNux { + pub message: String, +} + /// Metadata describing a Codex-supported model. #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ModelPreset { @@ -122,6 +128,8 @@ pub struct ModelPreset { pub upgrade: Option, /// Whether this preset should appear in the picker UI. pub show_in_picker: bool, + /// Availability NUX shown when this preset becomes accessible to the user. + pub availability_nux: Option, /// whether this model is supported in the api pub supported_in_api: bool, /// Input modalities accepted when composing user turns for this preset. @@ -224,11 +232,14 @@ pub struct ModelInfo { pub visibility: ModelVisibility, pub supported_in_api: bool, pub priority: i32, + pub availability_nux: Option, pub upgrade: Option, pub base_instructions: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub model_messages: Option, pub supports_reasoning_summaries: bool, + #[serde(default)] + pub default_reasoning_summary: ReasoningSummary, pub support_verbosity: bool, pub default_verbosity: Option, pub apply_patch_tool_type: Option, @@ -407,6 +418,7 @@ impl From for ModelPreset { migration_markdown: Some(upgrade.migration_markdown.clone()), }), show_in_picker: info.visibility == ModelVisibility::List, + availability_nux: info.availability_nux, supported_in_api: info.supported_in_api, input_modalities: info.input_modalities, } @@ -492,10 +504,12 @@ mod tests { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, + availability_nux: None, upgrade: None, base_instructions: "base".to_string(), model_messages: spec, supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, @@ -664,4 +678,57 @@ mod tests { ); assert_eq!(personality_variables.get_personality_message(None), None); } + + #[test] + fn model_info_defaults_availability_nux_to_none_when_omitted() { + let model: ModelInfo = serde_json::from_value(serde_json::json!({ + "slug": "test-model", + "display_name": "Test Model", + "description": null, + "supported_reasoning_levels": [], + "shell_type": "shell_command", + "visibility": "list", + "supported_in_api": true, + "priority": 1, + "upgrade": null, + "base_instructions": "base", + "model_messages": null, + "supports_reasoning_summaries": false, + "default_reasoning_summary": "auto", + "support_verbosity": false, + "default_verbosity": null, + "apply_patch_tool_type": null, + "truncation_policy": { + "mode": "bytes", + "limit": 10000 + }, + "supports_parallel_tool_calls": false, + "context_window": null, + "auto_compact_token_limit": null, + "effective_context_window_percent": 95, + "experimental_supported_tools": [], + "input_modalities": ["text", "image"], + "prefer_websockets": false + })) + .expect("deserialize model info"); + + assert_eq!(model.availability_nux, None); + } + + #[test] + fn model_preset_preserves_availability_nux() { + let preset = ModelPreset::from(ModelInfo { + availability_nux: Some(ModelAvailabilityNux { + message: "Try Spark.".to_string(), + }), + ..test_model(None) + }); + + assert_eq!( + preset.availability_nux, + Some(ModelAvailabilityNux { + message: "Try Spark.".to_string(), + }) + ); + } } diff --git a/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule_request_permission.md b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule_request_permission.md index 7a9f71e96a..1c9a3853a6 100644 --- a/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule_request_permission.md +++ b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule_request_permission.md @@ -8,8 +8,8 @@ When you need extra filesystem access for one command, use: - `sandbox_permissions: "with_additional_permissions"` - `additional_permissions` with one or both fields: - - `fs_read`: list of paths that need read access - - `fs_write`: list of paths that need write access + - `file_system.read`: list of paths that need read access + - `file_system.write`: list of paths that need write access This keeps execution inside the current sandbox policy, while adding only the requested permissions for that command, unless an exec-policy allow rule applies and authorizes running the command outside the sandbox. diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a8770cc90a..b5aaf47ea6 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -20,6 +20,7 @@ use crate::config_types::Personality; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::config_types::WindowsSandboxLevel; use crate::custom_prompts::CustomPrompt; +use crate::dynamic_tools::DynamicToolCallOutputContentItem; use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolSpec; @@ -40,7 +41,6 @@ use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; use crate::parse_command::ParsedCommand; use crate::plan_tool::UpdatePlanArgs; use crate::request_user_input::RequestUserInputResponse; -use crate::skill_approval::SkillApprovalResponse; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; @@ -61,7 +61,6 @@ pub use crate::approvals::NetworkApprovalProtocol; pub use crate::approvals::NetworkPolicyAmendment; pub use crate::approvals::NetworkPolicyRuleAction; pub use crate::request_user_input::RequestUserInputEvent; -pub use crate::skill_approval::SkillRequestApprovalEvent; /// Open/close tags for special user-input blocks. Used across crates to avoid /// duplicated hardcoded strings. @@ -186,7 +185,11 @@ pub enum Op { effort: Option, /// Will only be honored if the model is configured to use reasoning. - summary: ReasoningSummaryConfig, + /// + /// When omitted, the session keeps the current setting (which allows core to + /// fall back to the selected model's default on new sessions). + #[serde(default, skip_serializing_if = "Option::is_none")] + summary: Option, // The JSON schema to use for the final assistant message final_output_json_schema: Option, @@ -295,14 +298,6 @@ pub enum Op { response: DynamicToolResponse, }, - /// Resolve a skill approval request. - SkillApproval { - /// Item id for the in-flight request. - id: String, - /// User decision. - response: SkillApprovalResponse, - }, - /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has @@ -1053,7 +1048,7 @@ pub enum EventMsg { DynamicToolCallRequest(DynamicToolCallRequest), - SkillRequestApproval(SkillRequestApprovalEvent), + DynamicToolCallResponse(DynamicToolCallResponseEvent), ElicitationRequest(ElicitationRequestEvent), @@ -1781,6 +1776,27 @@ pub struct McpToolCallEndEvent { pub result: Result, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)] +pub struct DynamicToolCallResponseEvent { + /// Identifier for the corresponding DynamicToolCallRequest. + pub call_id: String, + /// Turn ID that this dynamic tool call belongs to. + pub turn_id: String, + /// Dynamic tool name. + pub tool: String, + /// Dynamic tool call arguments. + pub arguments: serde_json::Value, + /// Dynamic tool response content items. + pub content_items: Vec, + /// Whether the tool call succeeded. + pub success: bool, + /// Optional error text when the tool call failed before producing a response. + pub error: Option, + /// The duration of the dynamic tool call. + #[ts(type = "string")] + pub duration: Duration, +} + impl McpToolCallEndEvent { pub fn is_success(&self) -> bool { match &self.result { @@ -1976,7 +1992,7 @@ impl SessionSource { agent_nickname.clone() } SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => { - Some("morpheus".to_string()) + Some("Morpheus".to_string()) } _ => None, } @@ -2117,6 +2133,10 @@ pub struct TurnContextItem { #[serde(default, skip_serializing_if = "Option::is_none")] pub turn_id: Option, pub cwd: PathBuf, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_date: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timezone: Option, pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, #[serde(skip_serializing_if = "Option::is_none")] @@ -2765,8 +2785,8 @@ pub enum ReviewDecision { proposed_execpolicy_amendment: ExecPolicyAmendment, }, - /// User has approved this command and wants to automatically approve any - /// future identical instances (`command` and `cwd` match exactly) for the + /// User has approved this request and wants future prompts in the same + /// session-scoped approval cache to be automatically approved for the /// remainder of the session. ApprovedForSession, @@ -3345,6 +3365,8 @@ mod tests { let item = TurnContextItem { turn_id: None, cwd: PathBuf::from("/tmp"), + current_date: None, + timezone: None, approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::DangerFullAccess, network: Some(TurnContextNetworkItem { diff --git a/codex-rs/protocol/src/skill_approval.rs b/codex-rs/protocol/src/skill_approval.rs deleted file mode 100644 index 152e248186..0000000000 --- a/codex-rs/protocol/src/skill_approval.rs +++ /dev/null @@ -1,15 +0,0 @@ -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; -use ts_rs::TS; - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] -pub struct SkillRequestApprovalEvent { - pub item_id: String, - pub skill_name: String, -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] -pub struct SkillApprovalResponse { - pub approved: bool, -} diff --git a/codex-rs/protocol/src/user_input.rs b/codex-rs/protocol/src/user_input.rs index d40511f342..c1c22dcaf9 100644 --- a/codex-rs/protocol/src/user_input.rs +++ b/codex-rs/protocol/src/user_input.rs @@ -3,6 +3,9 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; +/// Conservative cap so one user message cannot monopolize a large context window. +pub const MAX_USER_INPUT_TEXT_CHARS: usize = 1 << 20; + /// User input #[non_exhaustive] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)] diff --git a/codex-rs/rmcp-client/src/perform_oauth_login.rs b/codex-rs/rmcp-client/src/perform_oauth_login.rs index 62a9c3b019..c71799c629 100644 --- a/codex-rs/rmcp-client/src/perform_oauth_login.rs +++ b/codex-rs/rmcp-client/src/perform_oauth_login.rs @@ -47,6 +47,7 @@ pub async fn perform_oauth_login( http_headers: Option>, env_http_headers: Option>, scopes: &[String], + oauth_resource: Option<&str>, callback_port: Option, callback_url: Option<&str>, ) -> Result<()> { @@ -60,6 +61,7 @@ pub async fn perform_oauth_login( store_mode, headers, scopes, + oauth_resource, true, callback_port, callback_url, @@ -78,6 +80,7 @@ pub async fn perform_oauth_login_return_url( http_headers: Option>, env_http_headers: Option>, scopes: &[String], + oauth_resource: Option<&str>, timeout_secs: Option, callback_port: Option, callback_url: Option<&str>, @@ -92,6 +95,7 @@ pub async fn perform_oauth_login_return_url( store_mode, headers, scopes, + oauth_resource, false, callback_port, callback_url, @@ -303,6 +307,7 @@ impl OauthLoginFlow { store_mode: OAuthCredentialsStoreMode, headers: OauthHeaders, scopes: &[String], + oauth_resource: Option<&str>, launch_browser: bool, callback_port: Option, callback_url: Option<&str>, @@ -340,7 +345,11 @@ impl OauthLoginFlow { oauth_state .start_authorization(&scope_refs, &redirect_uri, Some("Codex")) .await?; - let auth_url = oauth_state.get_authorization_url().await?; + let auth_url = append_query_param( + &oauth_state.get_authorization_url().await?, + "resource", + oauth_resource, + ); let timeout_secs = timeout_secs.unwrap_or(DEFAULT_OAUTH_TIMEOUT_SECS).max(1); let timeout = Duration::from_secs(timeout_secs as u64); @@ -431,9 +440,29 @@ impl OauthLoginFlow { } } +fn append_query_param(url: &str, key: &str, value: Option<&str>) -> String { + let Some(value) = value else { + return url.to_string(); + }; + let value = value.trim(); + if value.is_empty() { + return url.to_string(); + } + if let Ok(mut parsed) = Url::parse(url) { + parsed.query_pairs_mut().append_pair(key, value); + return parsed.to_string(); + } + let encoded = urlencoding::encode(value); + let separator = if url.contains('?') { "&" } else { "?" }; + format!("{url}{separator}{key}={encoded}") +} + #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; + use super::CallbackOutcome; + use super::append_query_param; use super::callback_path_from_redirect_uri; use super::parse_oauth_callback; @@ -461,4 +490,36 @@ mod tests { .expect("redirect URI should parse"); assert_eq!(path, "/oauth/callback"); } + + #[test] + fn append_query_param_adds_resource_to_absolute_url() { + let url = append_query_param( + "https://example.com/authorize?scope=read", + "resource", + Some("https://api.example.com"), + ); + + assert_eq!( + url, + "https://example.com/authorize?scope=read&resource=https%3A%2F%2Fapi.example.com" + ); + } + + #[test] + fn append_query_param_ignores_empty_values() { + let url = append_query_param( + "https://example.com/authorize?scope=read", + "resource", + Some(" "), + ); + + assert_eq!(url, "https://example.com/authorize?scope=read"); + } + + #[test] + fn append_query_param_handles_unparseable_url() { + let url = append_query_param("not a url", "resource", Some("api/resource")); + + assert_eq!(url, "not a url?resource=api%2Fresource"); + } } diff --git a/codex-rs/shell-escalation/Cargo.toml b/codex-rs/shell-escalation/Cargo.toml index f608b96083..fbc5bcd8c7 100644 --- a/codex-rs/shell-escalation/Cargo.toml +++ b/codex-rs/shell-escalation/Cargo.toml @@ -12,8 +12,9 @@ path = "src/bin/main_execve_wrapper.rs" anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } +codex-protocol = { workspace = true } +codex-utils-absolute-path = { workspace = true } libc = { workspace = true } -path-absolutize = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } socket2 = { workspace = true, features = ["all"] } diff --git a/codex-rs/shell-escalation/src/lib.rs b/codex-rs/shell-escalation/src/lib.rs index 48bc116581..1cc42a46db 100644 --- a/codex-rs/shell-escalation/src/lib.rs +++ b/codex-rs/shell-escalation/src/lib.rs @@ -6,12 +6,22 @@ pub use unix::EscalateAction; #[cfg(unix)] pub use unix::EscalateServer; #[cfg(unix)] +pub use unix::EscalationDecision; +#[cfg(unix)] +pub use unix::EscalationExecution; +#[cfg(unix)] +pub use unix::EscalationPermissions; +#[cfg(unix)] pub use unix::EscalationPolicy; #[cfg(unix)] pub use unix::ExecParams; #[cfg(unix)] pub use unix::ExecResult; #[cfg(unix)] +pub use unix::Permissions; +#[cfg(unix)] +pub use unix::PreparedExec; +#[cfg(unix)] pub use unix::ShellCommandExecutor; #[cfg(unix)] pub use unix::Stopwatch; diff --git a/codex-rs/shell-escalation/src/unix/escalate_client.rs b/codex-rs/shell-escalation/src/unix/escalate_client.rs index 5c363f7d6a..f9c866df4e 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_client.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_client.rs @@ -4,6 +4,7 @@ use std::os::fd::FromRawFd as _; use std::os::fd::OwnedFd; use anyhow::Context as _; +use codex_utils_absolute_path::AbsolutePathBuf; use crate::unix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR; use crate::unix::escalate_protocol::EXEC_WRAPPER_ENV_VAR; @@ -50,7 +51,7 @@ pub async fn run_shell_escalation_execve_wrapper( .send(EscalateRequest { file: file.clone().into(), argv: argv.clone(), - workdir: std::env::current_dir()?, + workdir: AbsolutePathBuf::current_dir()?, env, }) .await diff --git a/codex-rs/shell-escalation/src/unix/escalate_protocol.rs b/codex-rs/shell-escalation/src/unix/escalate_protocol.rs index 6be00fe4b2..ea38c7b2a6 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_protocol.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_protocol.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::os::fd::RawFd; use std::path::PathBuf; +use codex_protocol::approvals::EscalationPermissions; +use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; @@ -17,11 +19,14 @@ pub const LEGACY_BASH_EXEC_WRAPPER_ENV_VAR: &str = "BASH_EXEC_WRAPPER"; /// The client sends this to the server to request an exec() call. #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct EscalateRequest { - /// The absolute path to the executable to run, i.e. the first arg to exec. + /// The executable path from the intercepted exec call. + /// + /// This may be relative, in which case it should be resolved against + /// `workdir`. pub file: PathBuf, /// The argv, including the program name (argv[0]). pub argv: Vec, - pub workdir: PathBuf, + pub workdir: AbsolutePathBuf, pub env: HashMap, } @@ -31,6 +36,38 @@ pub struct EscalateResponse { pub action: EscalateAction, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EscalationDecision { + Run, + Escalate(EscalationExecution), + Deny { reason: Option }, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EscalationExecution { + /// Rerun the intercepted command outside any sandbox wrapper. + Unsandboxed, + /// Rerun using the turn's current sandbox configuration. + TurnDefault, + /// Rerun using an explicit sandbox configuration attached to the request. + Permissions(EscalationPermissions), +} + +impl EscalationDecision { + pub fn run() -> Self { + Self::Run + } + + pub fn escalate(execution: EscalationExecution) -> Self { + Self::Escalate(execution) + } + + pub fn deny(reason: Option) -> Self { + Self::Deny { reason } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum EscalateAction { /// The command should be run directly by the client. diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index 56db822ebb..bb45c3ed71 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Context as _; -use path_absolutize::Absolutize as _; +use codex_utils_absolute_path::AbsolutePathBuf; use tokio::process::Command; use tokio_util::sync::CancellationToken; @@ -15,6 +15,8 @@ use crate::unix::escalate_protocol::EXEC_WRAPPER_ENV_VAR; use crate::unix::escalate_protocol::EscalateAction; use crate::unix::escalate_protocol::EscalateRequest; use crate::unix::escalate_protocol::EscalateResponse; +use crate::unix::escalate_protocol::EscalationDecision; +use crate::unix::escalate_protocol::EscalationExecution; use crate::unix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR; use crate::unix::escalate_protocol::SuperExecMessage; use crate::unix::escalate_protocol::SuperExecResult; @@ -37,6 +39,16 @@ pub trait ShellCommandExecutor: Send + Sync { env: HashMap, cancel_rx: CancellationToken, ) -> anyhow::Result; + + /// Prepares an escalated subcommand for execution on the server side. + async fn prepare_escalated_exec( + &self, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, + env: HashMap, + execution: EscalationExecution, + ) -> anyhow::Result; } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -62,6 +74,14 @@ pub struct ExecResult { pub timed_out: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedExec { + pub command: Vec, + pub cwd: PathBuf, + pub env: HashMap, + pub arg0: Option, +} + pub struct EscalateServer { bash_path: PathBuf, execve_wrapper: PathBuf, @@ -69,9 +89,9 @@ pub struct EscalateServer { } impl EscalateServer { - pub fn new

(bash_path: PathBuf, execve_wrapper: PathBuf, policy: P) -> Self + pub fn new(bash_path: PathBuf, execve_wrapper: PathBuf, policy: Policy) -> Self where - P: EscalationPolicy + Send + Sync + 'static, + Policy: EscalationPolicy + Send + Sync + 'static, { Self { bash_path, @@ -84,13 +104,17 @@ impl EscalateServer { &self, params: ExecParams, cancel_rx: CancellationToken, - command_executor: &dyn ShellCommandExecutor, + command_executor: Arc, ) -> anyhow::Result { let (escalate_server, escalate_client) = AsyncDatagramSocket::pair()?; let client_socket = escalate_client.into_inner(); // Only the client endpoint should cross exec into the wrapper process. client_socket.set_cloexec(false)?; - let escalate_task = tokio::spawn(escalate_task(escalate_server, self.policy.clone())); + let escalate_task = tokio::spawn(escalate_task( + escalate_server, + Arc::clone(&self.policy), + Arc::clone(&command_executor), + )); let mut env = std::env::vars().collect::>(); env.insert( ESCALATE_SOCKET_ENV_VAR.to_string(), @@ -114,8 +138,9 @@ impl EscalateServer { }, params.command, ]; + let workdir = AbsolutePathBuf::try_from(params.workdir)?; let result = command_executor - .run(command, PathBuf::from(¶ms.workdir), env, cancel_rx) + .run(command, workdir.to_path_buf(), env, cancel_rx) .await?; escalate_task.abort(); Ok(result) @@ -125,6 +150,7 @@ impl EscalateServer { async fn escalate_task( socket: AsyncDatagramSocket, policy: Arc, + command_executor: Arc, ) -> anyhow::Result<()> { loop { let (_, mut fds) = socket.receive_with_fds().await?; @@ -133,9 +159,12 @@ async fn escalate_task( continue; } let stream_socket = AsyncSocket::from_fd(fds.remove(0))?; - let policy = policy.clone(); + let policy = Arc::clone(&policy); + let command_executor = Arc::clone(&command_executor); tokio::spawn(async move { - if let Err(err) = handle_escalate_session_with_policy(stream_socket, policy).await { + if let Err(err) = + handle_escalate_session_with_policy(stream_socket, policy, command_executor).await + { tracing::error!("escalate session failed: {err:?}"); } }); @@ -145,6 +174,7 @@ async fn escalate_task( async fn handle_escalate_session_with_policy( socket: AsyncSocket, policy: Arc, + command_executor: Arc, ) -> anyhow::Result<()> { let EscalateRequest { file, @@ -152,24 +182,23 @@ async fn handle_escalate_session_with_policy( workdir, env, } = socket.receive::().await?; - let file = PathBuf::from(&file).absolutize()?.into_owned(); - let workdir = PathBuf::from(&workdir).absolutize()?.into_owned(); - let action = policy - .determine_action(file.as_path(), &argv, &workdir) + let program = AbsolutePathBuf::resolve_path_against_base(file, workdir.as_path())?; + let decision = policy + .determine_action(&program, &argv, &workdir) .await .context("failed to determine escalation action")?; - tracing::debug!("decided {action:?} for {file:?} {argv:?} {workdir:?}"); + tracing::debug!("decided {decision:?} for {program:?} {argv:?} {workdir:?}"); - match action { - EscalateAction::Run => { + match decision { + EscalationDecision::Run => { socket .send(EscalateResponse { action: EscalateAction::Run, }) .await?; } - EscalateAction::Escalate => { + EscalationDecision::Escalate(execution) => { socket .send(EscalateResponse { action: EscalateAction::Escalate, @@ -197,12 +226,23 @@ async fn handle_escalate_session_with_policy( )); } - let mut command = Command::new(file); + let PreparedExec { + command, + cwd, + env, + arg0, + } = command_executor + .prepare_escalated_exec(&program, &argv, &workdir, env, execution) + .await?; + let (program, args) = command + .split_first() + .ok_or_else(|| anyhow::anyhow!("prepared escalated command must not be empty"))?; + let mut command = Command::new(program); command - .args(&argv[1..]) - .arg0(argv[0].clone()) + .args(args) + .arg0(arg0.unwrap_or_else(|| program.clone())) .envs(&env) - .current_dir(&workdir) + .current_dir(&cwd) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); @@ -222,7 +262,7 @@ async fn handle_escalate_session_with_policy( }) .await?; } - EscalateAction::Deny { reason } => { + EscalationDecision::Deny { reason } => { socket .send(EscalateResponse { action: EscalateAction::Deny { reason }, @@ -236,24 +276,117 @@ async fn handle_escalate_session_with_policy( #[cfg(test)] mod tests { use super::*; + use codex_protocol::approvals::EscalationPermissions; + use codex_protocol::models::PermissionProfile; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; - use std::path::Path; use std::path::PathBuf; struct DeterministicEscalationPolicy { - action: EscalateAction, + decision: EscalationDecision, } #[async_trait::async_trait] impl EscalationPolicy for DeterministicEscalationPolicy { async fn determine_action( &self, - _file: &Path, + _file: &AbsolutePathBuf, _argv: &[String], - _workdir: &Path, - ) -> anyhow::Result { - Ok(self.action.clone()) + _workdir: &AbsolutePathBuf, + ) -> anyhow::Result { + Ok(self.decision.clone()) + } + } + + struct AssertingEscalationPolicy { + expected_file: AbsolutePathBuf, + expected_workdir: AbsolutePathBuf, + } + + #[async_trait::async_trait] + impl EscalationPolicy for AssertingEscalationPolicy { + async fn determine_action( + &self, + file: &AbsolutePathBuf, + _argv: &[String], + workdir: &AbsolutePathBuf, + ) -> anyhow::Result { + assert_eq!(file, &self.expected_file); + assert_eq!(workdir, &self.expected_workdir); + Ok(EscalationDecision::run()) + } + } + + struct ForwardingShellCommandExecutor; + + #[async_trait::async_trait] + impl ShellCommandExecutor for ForwardingShellCommandExecutor { + async fn run( + &self, + _command: Vec, + _cwd: PathBuf, + _env: HashMap, + _cancel_rx: CancellationToken, + ) -> anyhow::Result { + unreachable!("run() is not used by handle_escalate_session_with_policy() tests") + } + + async fn prepare_escalated_exec( + &self, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, + env: HashMap, + _execution: EscalationExecution, + ) -> anyhow::Result { + Ok(PreparedExec { + command: std::iter::once(program.to_string_lossy().to_string()) + .chain(argv.iter().skip(1).cloned()) + .collect(), + cwd: workdir.to_path_buf(), + env, + arg0: argv.first().cloned(), + }) + } + } + + struct PermissionAssertingShellCommandExecutor { + expected_permissions: EscalationPermissions, + } + + #[async_trait::async_trait] + impl ShellCommandExecutor for PermissionAssertingShellCommandExecutor { + async fn run( + &self, + _command: Vec, + _cwd: PathBuf, + _env: HashMap, + _cancel_rx: CancellationToken, + ) -> anyhow::Result { + unreachable!("run() is not used by handle_escalate_session_with_policy() tests") + } + + async fn prepare_escalated_exec( + &self, + program: &AbsolutePathBuf, + argv: &[String], + workdir: &AbsolutePathBuf, + env: HashMap, + execution: EscalationExecution, + ) -> anyhow::Result { + assert_eq!( + execution, + EscalationExecution::Permissions(self.expected_permissions.clone()) + ); + Ok(PreparedExec { + command: std::iter::once(program.to_string_lossy().to_string()) + .chain(argv.iter().skip(1).cloned()) + .collect(), + cwd: workdir.to_path_buf(), + env, + arg0: argv.first().cloned(), + }) } } @@ -263,8 +396,9 @@ mod tests { let server_task = tokio::spawn(handle_escalate_session_with_policy( server, Arc::new(DeterministicEscalationPolicy { - action: EscalateAction::Run, + decision: EscalationDecision::run(), }), + Arc::new(ForwardingShellCommandExecutor), )); let mut env = HashMap::new(); @@ -277,7 +411,7 @@ mod tests { .send(EscalateRequest { file: PathBuf::from("/bin/echo"), argv: vec!["echo".to_string()], - workdir: PathBuf::from("/tmp"), + workdir: AbsolutePathBuf::try_from(PathBuf::from("/tmp"))?, env, }) .await?; @@ -292,14 +426,52 @@ mod tests { server_task.await? } + #[tokio::test] + async fn handle_escalate_session_resolves_relative_file_against_request_workdir() + -> anyhow::Result<()> { + let (server, client) = AsyncSocket::pair()?; + let tmp = tempfile::TempDir::new()?; + let workdir = tmp.path().join("workspace"); + std::fs::create_dir(&workdir)?; + let workdir = AbsolutePathBuf::try_from(workdir)?; + let expected_file = workdir.join("bin/tool")?; + let server_task = tokio::spawn(handle_escalate_session_with_policy( + server, + Arc::new(AssertingEscalationPolicy { + expected_file, + expected_workdir: workdir.clone(), + }), + Arc::new(ForwardingShellCommandExecutor), + )); + + client + .send(EscalateRequest { + file: PathBuf::from("./bin/tool"), + argv: vec!["./bin/tool".to_string()], + workdir, + env: HashMap::new(), + }) + .await?; + + let response = client.receive::().await?; + assert_eq!( + EscalateResponse { + action: EscalateAction::Run, + }, + response + ); + server_task.await? + } + #[tokio::test] async fn handle_escalate_session_executes_escalated_command() -> anyhow::Result<()> { let (server, client) = AsyncSocket::pair()?; let server_task = tokio::spawn(handle_escalate_session_with_policy( server, Arc::new(DeterministicEscalationPolicy { - action: EscalateAction::Escalate, + decision: EscalationDecision::escalate(EscalationExecution::Unsandboxed), }), + Arc::new(ForwardingShellCommandExecutor), )); client @@ -310,7 +482,7 @@ mod tests { "-c".to_string(), r#"if [ "$KEY" = VALUE ]; then exit 42; else exit 1; fi"#.to_string(), ], - workdir: std::env::current_dir()?, + workdir: AbsolutePathBuf::current_dir()?, env: HashMap::from([("KEY".to_string(), "VALUE".to_string())]), }) .await?; @@ -332,4 +504,52 @@ mod tests { server_task.await? } + + #[tokio::test] + async fn handle_escalate_session_passes_permissions_to_executor() -> anyhow::Result<()> { + let (server, client) = AsyncSocket::pair()?; + let server_task = tokio::spawn(handle_escalate_session_with_policy( + server, + Arc::new(DeterministicEscalationPolicy { + decision: EscalationDecision::escalate(EscalationExecution::Permissions( + EscalationPermissions::PermissionProfile(PermissionProfile { + network: Some(true), + ..Default::default() + }), + )), + }), + Arc::new(PermissionAssertingShellCommandExecutor { + expected_permissions: EscalationPermissions::PermissionProfile(PermissionProfile { + network: Some(true), + ..Default::default() + }), + }), + )); + + client + .send(EscalateRequest { + file: PathBuf::from("/bin/sh"), + argv: vec!["sh".to_string(), "-c".to_string(), "exit 0".to_string()], + workdir: AbsolutePathBuf::current_dir()?, + env: HashMap::new(), + }) + .await?; + + let response = client.receive::().await?; + assert_eq!( + EscalateResponse { + action: EscalateAction::Escalate, + }, + response + ); + + client + .send_with_fds(SuperExecMessage { fds: Vec::new() }, &[]) + .await?; + + let result = client.receive::().await?; + assert_eq!(0, result.exit_code); + + server_task.await? + } } diff --git a/codex-rs/shell-escalation/src/unix/escalation_policy.rs b/codex-rs/shell-escalation/src/unix/escalation_policy.rs index 09e74e2719..c3da252dd9 100644 --- a/codex-rs/shell-escalation/src/unix/escalation_policy.rs +++ b/codex-rs/shell-escalation/src/unix/escalation_policy.rs @@ -1,14 +1,14 @@ -use std::path::Path; +use codex_utils_absolute_path::AbsolutePathBuf; -use crate::unix::escalate_protocol::EscalateAction; +use crate::unix::escalate_protocol::EscalationDecision; /// Decides what action to take in response to an execve request from a client. #[async_trait::async_trait] pub trait EscalationPolicy: Send + Sync { async fn determine_action( &self, - file: &Path, + file: &AbsolutePathBuf, argv: &[String], - workdir: &Path, - ) -> anyhow::Result; + workdir: &AbsolutePathBuf, + ) -> anyhow::Result; } diff --git a/codex-rs/shell-escalation/src/unix/mod.rs b/codex-rs/shell-escalation/src/unix/mod.rs index 37e29e8775..6de12297a4 100644 --- a/codex-rs/shell-escalation/src/unix/mod.rs +++ b/codex-rs/shell-escalation/src/unix/mod.rs @@ -63,10 +63,15 @@ pub mod stopwatch; pub use self::escalate_client::run_shell_escalation_execve_wrapper; pub use self::escalate_protocol::EscalateAction; +pub use self::escalate_protocol::EscalationDecision; +pub use self::escalate_protocol::EscalationExecution; pub use self::escalate_server::EscalateServer; pub use self::escalate_server::ExecParams; pub use self::escalate_server::ExecResult; +pub use self::escalate_server::PreparedExec; pub use self::escalate_server::ShellCommandExecutor; pub use self::escalation_policy::EscalationPolicy; pub use self::execve_wrapper::main_execve_wrapper; pub use self::stopwatch::Stopwatch; +pub use codex_protocol::approvals::EscalationPermissions; +pub use codex_protocol::approvals::Permissions; diff --git a/codex-rs/state/migrations/0016_memory_usage.sql b/codex-rs/state/migrations/0016_memory_usage.sql new file mode 100644 index 0000000000..1067ab2e75 --- /dev/null +++ b/codex-rs/state/migrations/0016_memory_usage.sql @@ -0,0 +1,2 @@ +ALTER TABLE stage1_outputs ADD COLUMN usage_count INTEGER; +ALTER TABLE stage1_outputs ADD COLUMN last_usage INTEGER; diff --git a/codex-rs/state/migrations/0017_phase2_selection_flag.sql b/codex-rs/state/migrations/0017_phase2_selection_flag.sql new file mode 100644 index 0000000000..1d95d9a40e --- /dev/null +++ b/codex-rs/state/migrations/0017_phase2_selection_flag.sql @@ -0,0 +1,2 @@ +ALTER TABLE stage1_outputs +ADD COLUMN selected_for_phase2 INTEGER NOT NULL DEFAULT 0; diff --git a/codex-rs/state/migrations/0018_phase2_selection_snapshot.sql b/codex-rs/state/migrations/0018_phase2_selection_snapshot.sql new file mode 100644 index 0000000000..f175980fc7 --- /dev/null +++ b/codex-rs/state/migrations/0018_phase2_selection_snapshot.sql @@ -0,0 +1,3 @@ +ALTER TABLE stage1_outputs +ADD COLUMN selected_for_phase2_source_updated_at INTEGER; +ALTER TABLE threads ADD COLUMN memory_mode TEXT NOT NULL DEFAULT 'enabled'; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 0ba3df6790..f978238979 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -56,7 +56,9 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi } fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) { - metadata.cwd = turn_ctx.cwd.clone(); + if metadata.cwd.as_os_str().is_empty() { + metadata.cwd = turn_ctx.cwd.clone(); + } metadata.sandbox_policy = enum_to_string(&turn_ctx.sandbox_policy); metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy); } @@ -125,10 +127,17 @@ mod tests { use chrono::DateTime; use chrono::Utc; use codex_protocol::ThreadId; + use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; + use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::RolloutItem; + use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::protocol::SessionMeta; + use codex_protocol::protocol::SessionMetaLine; + use codex_protocol::protocol::SessionSource; + use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::USER_MESSAGE_BEGIN; use codex_protocol::protocol::UserMessageEvent; @@ -209,6 +218,97 @@ mod tests { assert_eq!(metadata.title, ""); } + #[test] + fn turn_context_does_not_override_session_cwd() { + let mut metadata = metadata_for_test(); + metadata.cwd = PathBuf::new(); + let thread_id = metadata.id; + + apply_rollout_item( + &mut metadata, + &RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + id: thread_id, + forked_from_id: Some( + ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"), + ), + timestamp: "2026-02-26T00:00:00.000Z".to_string(), + cwd: PathBuf::from("/child/worktree"), + originator: "codex_cli_rs".to_string(), + cli_version: "0.0.0".to_string(), + source: SessionSource::Cli, + agent_nickname: None, + agent_role: None, + model_provider: Some("openai".to_string()), + base_instructions: None, + dynamic_tools: None, + }, + git: None, + }), + "test-provider", + ); + apply_rollout_item( + &mut metadata, + &RolloutItem::TurnContext(TurnContextItem { + turn_id: Some("turn-1".to_string()), + cwd: PathBuf::from("/parent/workspace"), + current_date: None, + timezone: None, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + network: None, + model: "gpt-5".to_string(), + personality: None, + collaboration_mode: None, + effort: None, + summary: ReasoningSummary::Auto, + user_instructions: None, + developer_instructions: None, + final_output_json_schema: None, + truncation_policy: None, + }), + "test-provider", + ); + + assert_eq!(metadata.cwd, PathBuf::from("/child/worktree")); + assert_eq!( + metadata.sandbox_policy, + super::enum_to_string(&SandboxPolicy::DangerFullAccess) + ); + assert_eq!(metadata.approval_mode, "never"); + } + + #[test] + fn turn_context_sets_cwd_when_session_cwd_missing() { + let mut metadata = metadata_for_test(); + metadata.cwd = PathBuf::new(); + + apply_rollout_item( + &mut metadata, + &RolloutItem::TurnContext(TurnContextItem { + turn_id: Some("turn-1".to_string()), + cwd: PathBuf::from("/fallback/workspace"), + current_date: None, + timezone: None, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + network: None, + model: "gpt-5".to_string(), + personality: None, + collaboration_mode: None, + effort: None, + summary: ReasoningSummary::Auto, + user_instructions: None, + developer_instructions: None, + final_output_json_schema: None, + truncation_policy: None, + }), + "test-provider", + ); + + assert_eq!(metadata.cwd, PathBuf::from("/fallback/workspace")); + } + fn metadata_for_test() -> ThreadMetadata { let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id"); let created_at = DateTime::::from_timestamp(1_735_689_600, 0).expect("timestamp"); diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index 59607f3d9b..a6d1deeb84 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -14,6 +14,7 @@ mod runtime; pub use model::LogEntry; pub use model::LogQuery; pub use model::LogRow; +pub use model::Phase2InputSelection; pub use model::Phase2JobClaimOutcome; /// Preferred entrypoint: owns configuration and metrics. pub use runtime::StateRuntime; @@ -38,6 +39,7 @@ pub use model::SortKey; pub use model::Stage1JobClaim; pub use model::Stage1JobClaimOutcome; pub use model::Stage1Output; +pub use model::Stage1OutputRef; pub use model::Stage1StartupClaimParams; pub use model::ThreadMetadata; pub use model::ThreadMetadataBuilder; diff --git a/codex-rs/state/src/model/memories.rs b/codex-rs/state/src/model/memories.rs index 813c999395..0e663bf904 100644 --- a/codex-rs/state/src/model/memories.rs +++ b/codex-rs/state/src/model/memories.rs @@ -12,22 +12,41 @@ use super::ThreadMetadata; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Stage1Output { pub thread_id: ThreadId, + pub rollout_path: PathBuf, pub source_updated_at: DateTime, pub raw_memory: String, pub rollout_summary: String, pub rollout_slug: Option, pub cwd: PathBuf, + pub git_branch: Option, pub generated_at: DateTime, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Stage1OutputRef { + pub thread_id: ThreadId, + pub source_updated_at: DateTime, + pub rollout_slug: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Phase2InputSelection { + pub selected: Vec, + pub previous_selected: Vec, + pub retained_thread_ids: Vec, + pub removed: Vec, +} + #[derive(Debug)] pub(crate) struct Stage1OutputRow { thread_id: String, + rollout_path: String, source_updated_at: i64, raw_memory: String, rollout_summary: String, rollout_slug: Option, cwd: String, + git_branch: Option, generated_at: i64, } @@ -35,11 +54,13 @@ impl Stage1OutputRow { pub(crate) fn try_from_row(row: &SqliteRow) -> Result { Ok(Self { thread_id: row.try_get("thread_id")?, + rollout_path: row.try_get("rollout_path")?, source_updated_at: row.try_get("source_updated_at")?, raw_memory: row.try_get("raw_memory")?, rollout_summary: row.try_get("rollout_summary")?, rollout_slug: row.try_get("rollout_slug")?, cwd: row.try_get("cwd")?, + git_branch: row.try_get("git_branch")?, generated_at: row.try_get("generated_at")?, }) } @@ -51,11 +72,13 @@ impl TryFrom for Stage1Output { fn try_from(row: Stage1OutputRow) -> std::result::Result { Ok(Self { thread_id: ThreadId::try_from(row.thread_id)?, + rollout_path: PathBuf::from(row.rollout_path), source_updated_at: epoch_seconds_to_datetime(row.source_updated_at)?, raw_memory: row.raw_memory, rollout_summary: row.rollout_summary, rollout_slug: row.rollout_slug, cwd: PathBuf::from(row.cwd), + git_branch: row.git_branch, generated_at: epoch_seconds_to_datetime(row.generated_at)?, }) } @@ -66,6 +89,18 @@ fn epoch_seconds_to_datetime(secs: i64) -> Result> { .ok_or_else(|| anyhow::anyhow!("invalid unix timestamp: {secs}")) } +pub(crate) fn stage1_output_ref_from_parts( + thread_id: String, + source_updated_at: i64, + rollout_slug: Option, +) -> Result { + Ok(Stage1OutputRef { + thread_id: ThreadId::try_from(thread_id)?, + source_updated_at: epoch_seconds_to_datetime(source_updated_at)?, + rollout_slug, + }) +} + /// Result of trying to claim a stage-1 memory extraction job. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Stage1JobClaimOutcome { diff --git a/codex-rs/state/src/model/mod.rs b/codex-rs/state/src/model/mod.rs index 816c036f8b..efaf3f787e 100644 --- a/codex-rs/state/src/model/mod.rs +++ b/codex-rs/state/src/model/mod.rs @@ -16,10 +16,12 @@ pub use backfill_state::BackfillStatus; pub use log::LogEntry; pub use log::LogQuery; pub use log::LogRow; +pub use memories::Phase2InputSelection; pub use memories::Phase2JobClaimOutcome; pub use memories::Stage1JobClaim; pub use memories::Stage1JobClaimOutcome; pub use memories::Stage1Output; +pub use memories::Stage1OutputRef; pub use memories::Stage1StartupClaimParams; pub use thread_metadata::Anchor; pub use thread_metadata::BackfillStats; @@ -32,6 +34,7 @@ pub use thread_metadata::ThreadsPage; pub(crate) use agent_job::AgentJobItemRow; pub(crate) use agent_job::AgentJobRow; pub(crate) use memories::Stage1OutputRow; +pub(crate) use memories::stage1_output_ref_from_parts; pub(crate) use thread_metadata::ThreadRow; pub(crate) use thread_metadata::anchor_from_item; pub(crate) use thread_metadata::datetime_to_epoch_seconds; diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 29d85cdd52..42d235fc76 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -18,7 +18,6 @@ use crate::ThreadMetadataBuilder; use crate::ThreadsPage; use crate::apply_rollout_item; use crate::migrations::MIGRATOR; -use crate::model::AgentJobItemRow; use crate::model::AgentJobRow; use crate::model::ThreadRow; use crate::model::anchor_from_item; @@ -48,10 +47,14 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tracing::warn; -use uuid::Uuid; +mod agent_jobs; +mod backfill; +mod logs; mod memories; -// Memory-specific CRUD and phase job lifecycle methods live in `runtime/memories.rs`. +#[cfg(test)] +mod test_support; +mod threads; // "Partition" is the retention bucket we cap at 10 MiB: // - one bucket per non-null thread_id @@ -107,1457 +110,6 @@ impl StateRuntime { pub fn codex_home(&self) -> &Path { self.codex_home.as_path() } - - /// Get persisted rollout metadata backfill state. - pub async fn get_backfill_state(&self) -> anyhow::Result { - self.ensure_backfill_state_row().await?; - let row = sqlx::query( - r#" -SELECT status, last_watermark, last_success_at -FROM backfill_state -WHERE id = 1 - "#, - ) - .fetch_one(self.pool.as_ref()) - .await?; - crate::BackfillState::try_from_row(&row) - } - - /// Attempt to claim ownership of rollout metadata backfill. - /// - /// Returns `true` when this runtime claimed the backfill worker slot. - /// Returns `false` if backfill is already complete or currently owned by a - /// non-expired worker. - pub async fn try_claim_backfill(&self, lease_seconds: i64) -> anyhow::Result { - self.ensure_backfill_state_row().await?; - let now = Utc::now().timestamp(); - let lease_cutoff = now.saturating_sub(lease_seconds.max(0)); - let result = sqlx::query( - r#" -UPDATE backfill_state -SET status = ?, updated_at = ? -WHERE id = 1 - AND status != ? - AND (status != ? OR updated_at <= ?) - "#, - ) - .bind(crate::BackfillStatus::Running.as_str()) - .bind(now) - .bind(crate::BackfillStatus::Complete.as_str()) - .bind(crate::BackfillStatus::Running.as_str()) - .bind(lease_cutoff) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() == 1) - } - - /// Mark rollout metadata backfill as running. - pub async fn mark_backfill_running(&self) -> anyhow::Result<()> { - self.ensure_backfill_state_row().await?; - sqlx::query( - r#" -UPDATE backfill_state -SET status = ?, updated_at = ? -WHERE id = 1 - "#, - ) - .bind(crate::BackfillStatus::Running.as_str()) - .bind(Utc::now().timestamp()) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - /// Persist rollout metadata backfill progress. - pub async fn checkpoint_backfill(&self, watermark: &str) -> anyhow::Result<()> { - self.ensure_backfill_state_row().await?; - sqlx::query( - r#" -UPDATE backfill_state -SET status = ?, last_watermark = ?, updated_at = ? -WHERE id = 1 - "#, - ) - .bind(crate::BackfillStatus::Running.as_str()) - .bind(watermark) - .bind(Utc::now().timestamp()) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - /// Mark rollout metadata backfill as complete. - pub async fn mark_backfill_complete(&self, last_watermark: Option<&str>) -> anyhow::Result<()> { - self.ensure_backfill_state_row().await?; - let now = Utc::now().timestamp(); - sqlx::query( - r#" -UPDATE backfill_state -SET - status = ?, - last_watermark = COALESCE(?, last_watermark), - last_success_at = ?, - updated_at = ? -WHERE id = 1 - "#, - ) - .bind(crate::BackfillStatus::Complete.as_str()) - .bind(last_watermark) - .bind(now) - .bind(now) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - /// Load thread metadata by id using the underlying database. - pub async fn get_thread(&self, id: ThreadId) -> anyhow::Result> { - let row = sqlx::query( - r#" -SELECT - id, - rollout_path, - created_at, - updated_at, - source, - agent_nickname, - agent_role, - model_provider, - cwd, - cli_version, - title, - sandbox_policy, - approval_mode, - tokens_used, - first_user_message, - archived_at, - git_sha, - git_branch, - git_origin_url -FROM threads -WHERE id = ? - "#, - ) - .bind(id.to_string()) - .fetch_optional(self.pool.as_ref()) - .await?; - row.map(|row| ThreadRow::try_from_row(&row).and_then(ThreadMetadata::try_from)) - .transpose() - } - - /// Get dynamic tools for a thread, if present. - pub async fn get_dynamic_tools( - &self, - thread_id: ThreadId, - ) -> anyhow::Result>> { - let rows = sqlx::query( - r#" -SELECT name, description, input_schema -FROM thread_dynamic_tools -WHERE thread_id = ? -ORDER BY position ASC - "#, - ) - .bind(thread_id.to_string()) - .fetch_all(self.pool.as_ref()) - .await?; - if rows.is_empty() { - return Ok(None); - } - let mut tools = Vec::with_capacity(rows.len()); - for row in rows { - let input_schema: String = row.try_get("input_schema")?; - let input_schema = serde_json::from_str::(input_schema.as_str())?; - tools.push(DynamicToolSpec { - name: row.try_get("name")?, - description: row.try_get("description")?, - input_schema, - }); - } - Ok(Some(tools)) - } - - /// Find a rollout path by thread id using the underlying database. - pub async fn find_rollout_path_by_id( - &self, - id: ThreadId, - archived_only: Option, - ) -> anyhow::Result> { - let mut builder = - QueryBuilder::::new("SELECT rollout_path FROM threads WHERE id = "); - builder.push_bind(id.to_string()); - match archived_only { - Some(true) => { - builder.push(" AND archived = 1"); - } - Some(false) => { - builder.push(" AND archived = 0"); - } - None => {} - } - let row = builder.build().fetch_optional(self.pool.as_ref()).await?; - Ok(row - .and_then(|r| r.try_get::("rollout_path").ok()) - .map(PathBuf::from)) - } - - /// List threads using the underlying database. - pub async fn list_threads( - &self, - page_size: usize, - anchor: Option<&crate::Anchor>, - sort_key: crate::SortKey, - allowed_sources: &[String], - model_providers: Option<&[String]>, - archived_only: bool, - ) -> anyhow::Result { - let limit = page_size.saturating_add(1); - - let mut builder = QueryBuilder::::new( - r#" -SELECT - id, - rollout_path, - created_at, - updated_at, - source, - agent_nickname, - agent_role, - model_provider, - cwd, - cli_version, - title, - sandbox_policy, - approval_mode, - tokens_used, - first_user_message, - archived_at, - git_sha, - git_branch, - git_origin_url -FROM threads - "#, - ); - push_thread_filters( - &mut builder, - archived_only, - allowed_sources, - model_providers, - anchor, - sort_key, - ); - push_thread_order_and_limit(&mut builder, sort_key, limit); - - let rows = builder.build().fetch_all(self.pool.as_ref()).await?; - let mut items = rows - .into_iter() - .map(|row| ThreadRow::try_from_row(&row).and_then(ThreadMetadata::try_from)) - .collect::, _>>()?; - let num_scanned_rows = items.len(); - let next_anchor = if items.len() > page_size { - items.pop(); - items - .last() - .and_then(|item| anchor_from_item(item, sort_key)) - } else { - None - }; - Ok(ThreadsPage { - items, - next_anchor, - num_scanned_rows, - }) - } - - /// Insert one log entry into the logs table. - pub async fn insert_log(&self, entry: &LogEntry) -> anyhow::Result<()> { - self.insert_logs(std::slice::from_ref(entry)).await - } - - /// Insert a batch of log entries into the logs table. - pub async fn insert_logs(&self, entries: &[LogEntry]) -> anyhow::Result<()> { - if entries.is_empty() { - return Ok(()); - } - - let mut tx = self.pool.begin().await?; - let mut builder = QueryBuilder::::new( - "INSERT INTO logs (ts, ts_nanos, level, target, message, thread_id, process_uuid, module_path, file, line, estimated_bytes) ", - ); - builder.push_values(entries, |mut row, entry| { - let estimated_bytes = entry.message.as_ref().map_or(0, String::len) as i64 - + entry.level.len() as i64 - + entry.target.len() as i64 - + entry.module_path.as_ref().map_or(0, String::len) as i64 - + entry.file.as_ref().map_or(0, String::len) as i64; - row.push_bind(entry.ts) - .push_bind(entry.ts_nanos) - .push_bind(&entry.level) - .push_bind(&entry.target) - .push_bind(&entry.message) - .push_bind(&entry.thread_id) - .push_bind(&entry.process_uuid) - .push_bind(&entry.module_path) - .push_bind(&entry.file) - .push_bind(entry.line) - .push_bind(estimated_bytes); - }); - builder.build().execute(&mut *tx).await?; - self.prune_logs_after_insert(entries, &mut tx).await?; - tx.commit().await?; - Ok(()) - } - - /// Enforce per-partition log size caps after a successful batch insert. - /// - /// We maintain two independent budgets: - /// - Thread logs: rows with `thread_id IS NOT NULL`, capped per `thread_id`. - /// - Threadless process logs: rows with `thread_id IS NULL` ("threadless"), - /// capped per `process_uuid` (including `process_uuid IS NULL` as its own - /// threadless partition). - /// - /// "Threadless" means the log row is not associated with any conversation - /// thread, so retention is keyed by process identity instead. - /// - /// This runs inside the same transaction as the insert so callers never - /// observe "inserted but not yet pruned" rows. - async fn prune_logs_after_insert( - &self, - entries: &[LogEntry], - tx: &mut SqliteConnection, - ) -> anyhow::Result<()> { - let thread_ids: BTreeSet<&str> = entries - .iter() - .filter_map(|entry| entry.thread_id.as_deref()) - .collect(); - if !thread_ids.is_empty() { - // Cheap precheck: only run the heavier window-function prune for - // threads that are currently above the cap. - let mut over_limit_threads_query = - QueryBuilder::::new("SELECT thread_id FROM logs WHERE thread_id IN ("); - { - let mut separated = over_limit_threads_query.separated(", "); - for thread_id in &thread_ids { - separated.push_bind(*thread_id); - } - } - over_limit_threads_query.push(") GROUP BY thread_id HAVING SUM("); - over_limit_threads_query.push("estimated_bytes"); - over_limit_threads_query.push(") > "); - over_limit_threads_query.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); - let over_limit_thread_ids: Vec = over_limit_threads_query - .build() - .fetch_all(&mut *tx) - .await? - .into_iter() - .map(|row| row.try_get("thread_id")) - .collect::>()?; - if !over_limit_thread_ids.is_empty() { - // Enforce a strict per-thread cap by deleting every row whose - // newest-first cumulative bytes exceed the partition budget. - let mut prune_threads = QueryBuilder::::new( - r#" -DELETE FROM logs -WHERE id IN ( - SELECT id - FROM ( - SELECT - id, - SUM( -"#, - ); - prune_threads.push("estimated_bytes"); - prune_threads.push( - r#" - ) OVER ( - PARTITION BY thread_id - ORDER BY ts DESC, ts_nanos DESC, id DESC - ) AS cumulative_bytes - FROM logs - WHERE thread_id IN ( -"#, - ); - { - let mut separated = prune_threads.separated(", "); - for thread_id in &over_limit_thread_ids { - separated.push_bind(thread_id); - } - } - prune_threads.push( - r#" - ) - ) - WHERE cumulative_bytes > -"#, - ); - prune_threads.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); - prune_threads.push("\n)"); - prune_threads.build().execute(&mut *tx).await?; - } - } - - let threadless_process_uuids: BTreeSet<&str> = entries - .iter() - .filter(|entry| entry.thread_id.is_none()) - .filter_map(|entry| entry.process_uuid.as_deref()) - .collect(); - let has_threadless_null_process_uuid = entries - .iter() - .any(|entry| entry.thread_id.is_none() && entry.process_uuid.is_none()); - if !threadless_process_uuids.is_empty() { - // Threadless logs are budgeted separately per process UUID. - let mut over_limit_processes_query = QueryBuilder::::new( - "SELECT process_uuid FROM logs WHERE thread_id IS NULL AND process_uuid IN (", - ); - { - let mut separated = over_limit_processes_query.separated(", "); - for process_uuid in &threadless_process_uuids { - separated.push_bind(*process_uuid); - } - } - over_limit_processes_query.push(") GROUP BY process_uuid HAVING SUM("); - over_limit_processes_query.push("estimated_bytes"); - over_limit_processes_query.push(") > "); - over_limit_processes_query.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); - let over_limit_process_uuids: Vec = over_limit_processes_query - .build() - .fetch_all(&mut *tx) - .await? - .into_iter() - .map(|row| row.try_get("process_uuid")) - .collect::>()?; - if !over_limit_process_uuids.is_empty() { - // Same strict cap policy as thread pruning, but only for - // threadless rows in the affected process UUIDs. - let mut prune_threadless_process_logs = QueryBuilder::::new( - r#" -DELETE FROM logs -WHERE id IN ( - SELECT id - FROM ( - SELECT - id, - SUM( -"#, - ); - prune_threadless_process_logs.push("estimated_bytes"); - prune_threadless_process_logs.push( - r#" - ) OVER ( - PARTITION BY process_uuid - ORDER BY ts DESC, ts_nanos DESC, id DESC - ) AS cumulative_bytes - FROM logs - WHERE thread_id IS NULL - AND process_uuid IN ( -"#, - ); - { - let mut separated = prune_threadless_process_logs.separated(", "); - for process_uuid in &over_limit_process_uuids { - separated.push_bind(process_uuid); - } - } - prune_threadless_process_logs.push( - r#" - ) - ) - WHERE cumulative_bytes > -"#, - ); - prune_threadless_process_logs.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); - prune_threadless_process_logs.push("\n)"); - prune_threadless_process_logs - .build() - .execute(&mut *tx) - .await?; - } - } - if has_threadless_null_process_uuid { - // Rows without a process UUID still need a cap; treat NULL as its - // own threadless partition. - let mut null_process_usage_query = QueryBuilder::::new("SELECT SUM("); - null_process_usage_query.push("estimated_bytes"); - null_process_usage_query.push( - ") AS total_bytes FROM logs WHERE thread_id IS NULL AND process_uuid IS NULL", - ); - let total_null_process_bytes: Option = null_process_usage_query - .build() - .fetch_one(&mut *tx) - .await? - .try_get("total_bytes")?; - - if total_null_process_bytes.unwrap_or(0) > LOG_PARTITION_SIZE_LIMIT_BYTES { - let mut prune_threadless_null_process_logs = QueryBuilder::::new( - r#" -DELETE FROM logs -WHERE id IN ( - SELECT id - FROM ( - SELECT - id, - SUM( -"#, - ); - prune_threadless_null_process_logs.push("estimated_bytes"); - prune_threadless_null_process_logs.push( - r#" - ) OVER ( - PARTITION BY process_uuid - ORDER BY ts DESC, ts_nanos DESC, id DESC - ) AS cumulative_bytes - FROM logs - WHERE thread_id IS NULL - AND process_uuid IS NULL - ) - WHERE cumulative_bytes > -"#, - ); - prune_threadless_null_process_logs.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); - prune_threadless_null_process_logs.push("\n)"); - prune_threadless_null_process_logs - .build() - .execute(&mut *tx) - .await?; - } - } - Ok(()) - } - - pub(crate) async fn delete_logs_before(&self, cutoff_ts: i64) -> anyhow::Result { - let result = sqlx::query("DELETE FROM logs WHERE ts < ?") - .bind(cutoff_ts) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected()) - } - - /// Query logs with optional filters. - pub async fn query_logs(&self, query: &LogQuery) -> anyhow::Result> { - let mut builder = QueryBuilder::::new( - "SELECT id, ts, ts_nanos, level, target, message, thread_id, process_uuid, file, line FROM logs WHERE 1 = 1", - ); - push_log_filters(&mut builder, query); - if query.descending { - builder.push(" ORDER BY id DESC"); - } else { - builder.push(" ORDER BY id ASC"); - } - if let Some(limit) = query.limit { - builder.push(" LIMIT ").push_bind(limit as i64); - } - - let rows = builder - .build_query_as::() - .fetch_all(self.pool.as_ref()) - .await?; - Ok(rows) - } - - /// Return the max log id matching optional filters. - pub async fn max_log_id(&self, query: &LogQuery) -> anyhow::Result { - let mut builder = - QueryBuilder::::new("SELECT MAX(id) AS max_id FROM logs WHERE 1 = 1"); - push_log_filters(&mut builder, query); - let row = builder.build().fetch_one(self.pool.as_ref()).await?; - let max_id: Option = row.try_get("max_id")?; - Ok(max_id.unwrap_or(0)) - } - - /// List thread ids using the underlying database (no rollout scanning). - pub async fn list_thread_ids( - &self, - limit: usize, - anchor: Option<&crate::Anchor>, - sort_key: crate::SortKey, - allowed_sources: &[String], - model_providers: Option<&[String]>, - archived_only: bool, - ) -> anyhow::Result> { - let mut builder = QueryBuilder::::new("SELECT id FROM threads"); - push_thread_filters( - &mut builder, - archived_only, - allowed_sources, - model_providers, - anchor, - sort_key, - ); - push_thread_order_and_limit(&mut builder, sort_key, limit); - - let rows = builder.build().fetch_all(self.pool.as_ref()).await?; - rows.into_iter() - .map(|row| { - let id: String = row.try_get("id")?; - Ok(ThreadId::try_from(id)?) - }) - .collect() - } - - /// Insert or replace thread metadata directly. - pub async fn upsert_thread(&self, metadata: &crate::ThreadMetadata) -> anyhow::Result<()> { - sqlx::query( - r#" -INSERT INTO threads ( - id, - rollout_path, - created_at, - updated_at, - source, - agent_nickname, - agent_role, - model_provider, - cwd, - cli_version, - title, - sandbox_policy, - approval_mode, - tokens_used, - first_user_message, - archived, - archived_at, - git_sha, - git_branch, - git_origin_url -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -ON CONFLICT(id) DO UPDATE SET - rollout_path = excluded.rollout_path, - created_at = excluded.created_at, - updated_at = excluded.updated_at, - source = excluded.source, - agent_nickname = excluded.agent_nickname, - agent_role = excluded.agent_role, - model_provider = excluded.model_provider, - cwd = excluded.cwd, - cli_version = excluded.cli_version, - title = excluded.title, - sandbox_policy = excluded.sandbox_policy, - approval_mode = excluded.approval_mode, - tokens_used = excluded.tokens_used, - first_user_message = excluded.first_user_message, - archived = excluded.archived, - archived_at = excluded.archived_at, - git_sha = excluded.git_sha, - git_branch = excluded.git_branch, - git_origin_url = excluded.git_origin_url - "#, - ) - .bind(metadata.id.to_string()) - .bind(metadata.rollout_path.display().to_string()) - .bind(datetime_to_epoch_seconds(metadata.created_at)) - .bind(datetime_to_epoch_seconds(metadata.updated_at)) - .bind(metadata.source.as_str()) - .bind(metadata.agent_nickname.as_deref()) - .bind(metadata.agent_role.as_deref()) - .bind(metadata.model_provider.as_str()) - .bind(metadata.cwd.display().to_string()) - .bind(metadata.cli_version.as_str()) - .bind(metadata.title.as_str()) - .bind(metadata.sandbox_policy.as_str()) - .bind(metadata.approval_mode.as_str()) - .bind(metadata.tokens_used) - .bind(metadata.first_user_message.as_deref().unwrap_or_default()) - .bind(metadata.archived_at.is_some()) - .bind(metadata.archived_at.map(datetime_to_epoch_seconds)) - .bind(metadata.git_sha.as_deref()) - .bind(metadata.git_branch.as_deref()) - .bind(metadata.git_origin_url.as_deref()) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - /// Persist dynamic tools for a thread if none have been stored yet. - /// - /// Dynamic tools are defined at thread start and should not change afterward. - /// This only writes the first time we see tools for a given thread. - pub async fn persist_dynamic_tools( - &self, - thread_id: ThreadId, - tools: Option<&[DynamicToolSpec]>, - ) -> anyhow::Result<()> { - let Some(tools) = tools else { - return Ok(()); - }; - if tools.is_empty() { - return Ok(()); - } - let thread_id = thread_id.to_string(); - let mut tx = self.pool.begin().await?; - for (idx, tool) in tools.iter().enumerate() { - let position = i64::try_from(idx).unwrap_or(i64::MAX); - let input_schema = serde_json::to_string(&tool.input_schema)?; - sqlx::query( - r#" -INSERT INTO thread_dynamic_tools ( - thread_id, - position, - name, - description, - input_schema -) VALUES (?, ?, ?, ?, ?) -ON CONFLICT(thread_id, position) DO NOTHING - "#, - ) - .bind(thread_id.as_str()) - .bind(position) - .bind(tool.name.as_str()) - .bind(tool.description.as_str()) - .bind(input_schema) - .execute(&mut *tx) - .await?; - } - tx.commit().await?; - Ok(()) - } - - /// Apply rollout items incrementally using the underlying database. - pub async fn apply_rollout_items( - &self, - builder: &ThreadMetadataBuilder, - items: &[RolloutItem], - otel: Option<&OtelManager>, - ) -> anyhow::Result<()> { - if items.is_empty() { - return Ok(()); - } - let mut metadata = self - .get_thread(builder.id) - .await? - .unwrap_or_else(|| builder.build(&self.default_provider)); - metadata.rollout_path = builder.rollout_path.clone(); - for item in items { - apply_rollout_item(&mut metadata, item, &self.default_provider); - } - if let Some(updated_at) = file_modified_time_utc(builder.rollout_path.as_path()).await { - metadata.updated_at = updated_at; - } - // Keep the thread upsert before dynamic tools to satisfy the foreign key constraint: - // thread_dynamic_tools.thread_id -> threads.id. - if let Err(err) = self.upsert_thread(&metadata).await { - if let Some(otel) = otel { - otel.counter(DB_ERROR_METRIC, 1, &[("stage", "apply_rollout_items")]); - } - return Err(err); - } - let dynamic_tools = extract_dynamic_tools(items); - if let Some(dynamic_tools) = dynamic_tools - && let Err(err) = self - .persist_dynamic_tools(builder.id, dynamic_tools.as_deref()) - .await - { - if let Some(otel) = otel { - otel.counter(DB_ERROR_METRIC, 1, &[("stage", "persist_dynamic_tools")]); - } - return Err(err); - } - Ok(()) - } - - /// Mark a thread as archived using the underlying database. - pub async fn mark_archived( - &self, - thread_id: ThreadId, - rollout_path: &Path, - archived_at: DateTime, - ) -> anyhow::Result<()> { - let Some(mut metadata) = self.get_thread(thread_id).await? else { - return Ok(()); - }; - metadata.archived_at = Some(archived_at); - metadata.rollout_path = rollout_path.to_path_buf(); - if let Some(updated_at) = file_modified_time_utc(rollout_path).await { - metadata.updated_at = updated_at; - } - if metadata.id != thread_id { - warn!( - "thread id mismatch during archive: expected {thread_id}, got {}", - metadata.id - ); - } - self.upsert_thread(&metadata).await - } - - /// Mark a thread as unarchived using the underlying database. - pub async fn mark_unarchived( - &self, - thread_id: ThreadId, - rollout_path: &Path, - ) -> anyhow::Result<()> { - let Some(mut metadata) = self.get_thread(thread_id).await? else { - return Ok(()); - }; - metadata.archived_at = None; - metadata.rollout_path = rollout_path.to_path_buf(); - if let Some(updated_at) = file_modified_time_utc(rollout_path).await { - metadata.updated_at = updated_at; - } - if metadata.id != thread_id { - warn!( - "thread id mismatch during unarchive: expected {thread_id}, got {}", - metadata.id - ); - } - self.upsert_thread(&metadata).await - } - - /// Delete a thread metadata row by id. - pub async fn delete_thread(&self, thread_id: ThreadId) -> anyhow::Result { - let result = sqlx::query("DELETE FROM threads WHERE id = ?") - .bind(thread_id.to_string()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected()) - } - - pub async fn create_agent_job( - &self, - params: &AgentJobCreateParams, - items: &[AgentJobItemCreateParams], - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let input_headers_json = serde_json::to_string(¶ms.input_headers)?; - let output_schema_json = params - .output_schema_json - .as_ref() - .map(serde_json::to_string) - .transpose()?; - let max_runtime_seconds = params - .max_runtime_seconds - .map(i64::try_from) - .transpose() - .map_err(|_| anyhow::anyhow!("invalid max_runtime_seconds value"))?; - let mut tx = self.pool.begin().await?; - sqlx::query( - r#" -INSERT INTO agent_jobs ( - id, - name, - status, - instruction, - auto_export, - max_runtime_seconds, - output_schema_json, - input_headers_json, - input_csv_path, - output_csv_path, - created_at, - updated_at, - started_at, - completed_at, - last_error -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL) - "#, - ) - .bind(params.id.as_str()) - .bind(params.name.as_str()) - .bind(AgentJobStatus::Pending.as_str()) - .bind(params.instruction.as_str()) - .bind(i64::from(params.auto_export)) - .bind(max_runtime_seconds) - .bind(output_schema_json) - .bind(input_headers_json) - .bind(params.input_csv_path.as_str()) - .bind(params.output_csv_path.as_str()) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await?; - - for item in items { - let row_json = serde_json::to_string(&item.row_json)?; - sqlx::query( - r#" -INSERT INTO agent_job_items ( - job_id, - item_id, - row_index, - source_id, - row_json, - status, - assigned_thread_id, - attempt_count, - result_json, - last_error, - created_at, - updated_at, - completed_at, - reported_at -) VALUES (?, ?, ?, ?, ?, ?, NULL, 0, NULL, NULL, ?, ?, NULL, NULL) - "#, - ) - .bind(params.id.as_str()) - .bind(item.item_id.as_str()) - .bind(item.row_index) - .bind(item.source_id.as_deref()) - .bind(row_json) - .bind(AgentJobItemStatus::Pending.as_str()) - .bind(now) - .bind(now) - .execute(&mut *tx) - .await?; - } - - tx.commit().await?; - - let job_id = params.id.as_str(); - self.get_agent_job(job_id) - .await? - .ok_or_else(|| anyhow::anyhow!("failed to load created agent job {job_id}")) - } - - pub async fn get_agent_job(&self, job_id: &str) -> anyhow::Result> { - let row = sqlx::query_as::<_, AgentJobRow>( - r#" -SELECT - id, - name, - status, - instruction, - auto_export, - max_runtime_seconds, - output_schema_json, - input_headers_json, - input_csv_path, - output_csv_path, - created_at, - updated_at, - started_at, - completed_at, - last_error -FROM agent_jobs -WHERE id = ? - "#, - ) - .bind(job_id) - .fetch_optional(self.pool.as_ref()) - .await?; - row.map(AgentJob::try_from).transpose() - } - - pub async fn list_agent_job_items( - &self, - job_id: &str, - status: Option, - limit: Option, - ) -> anyhow::Result> { - let mut builder = QueryBuilder::::new( - r#" -SELECT - job_id, - item_id, - row_index, - source_id, - row_json, - status, - assigned_thread_id, - attempt_count, - result_json, - last_error, - created_at, - updated_at, - completed_at, - reported_at -FROM agent_job_items -WHERE job_id = - "#, - ); - builder.push_bind(job_id); - if let Some(status) = status { - builder.push(" AND status = "); - builder.push_bind(status.as_str()); - } - builder.push(" ORDER BY row_index ASC"); - if let Some(limit) = limit { - builder.push(" LIMIT "); - builder.push_bind(limit as i64); - } - let rows = builder - .build_query_as::() - .fetch_all(self.pool.as_ref()) - .await?; - rows.into_iter().map(AgentJobItem::try_from).collect() - } - - pub async fn get_agent_job_item( - &self, - job_id: &str, - item_id: &str, - ) -> anyhow::Result> { - let row = sqlx::query_as::<_, AgentJobItemRow>( - r#" -SELECT - job_id, - item_id, - row_index, - source_id, - row_json, - status, - assigned_thread_id, - attempt_count, - result_json, - last_error, - created_at, - updated_at, - completed_at, - reported_at -FROM agent_job_items -WHERE job_id = ? AND item_id = ? - "#, - ) - .bind(job_id) - .bind(item_id) - .fetch_optional(self.pool.as_ref()) - .await?; - row.map(AgentJobItem::try_from).transpose() - } - - pub async fn mark_agent_job_running(&self, job_id: &str) -> anyhow::Result<()> { - let now = Utc::now().timestamp(); - sqlx::query( - r#" -UPDATE agent_jobs -SET - status = ?, - updated_at = ?, - started_at = COALESCE(started_at, ?), - completed_at = NULL, - last_error = NULL -WHERE id = ? - "#, - ) - .bind(AgentJobStatus::Running.as_str()) - .bind(now) - .bind(now) - .bind(job_id) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<()> { - let now = Utc::now().timestamp(); - sqlx::query( - r#" -UPDATE agent_jobs -SET status = ?, updated_at = ?, completed_at = ?, last_error = NULL -WHERE id = ? - "#, - ) - .bind(AgentJobStatus::Completed.as_str()) - .bind(now) - .bind(now) - .bind(job_id) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - pub async fn mark_agent_job_failed( - &self, - job_id: &str, - error_message: &str, - ) -> anyhow::Result<()> { - let now = Utc::now().timestamp(); - sqlx::query( - r#" -UPDATE agent_jobs -SET status = ?, updated_at = ?, completed_at = ?, last_error = ? -WHERE id = ? - "#, - ) - .bind(AgentJobStatus::Failed.as_str()) - .bind(now) - .bind(now) - .bind(error_message) - .bind(job_id) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } - - pub async fn mark_agent_job_cancelled( - &self, - job_id: &str, - reason: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_jobs -SET status = ?, updated_at = ?, completed_at = ?, last_error = ? -WHERE id = ? AND status IN (?, ?) - "#, - ) - .bind(AgentJobStatus::Cancelled.as_str()) - .bind(now) - .bind(now) - .bind(reason) - .bind(job_id) - .bind(AgentJobStatus::Pending.as_str()) - .bind(AgentJobStatus::Running.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn is_agent_job_cancelled(&self, job_id: &str) -> anyhow::Result { - let row = sqlx::query( - r#" -SELECT status -FROM agent_jobs -WHERE id = ? - "#, - ) - .bind(job_id) - .fetch_optional(self.pool.as_ref()) - .await?; - let Some(row) = row else { - return Ok(false); - }; - let status: String = row.try_get("status")?; - Ok(AgentJobStatus::parse(status.as_str())? == AgentJobStatus::Cancelled) - } - - pub async fn mark_agent_job_item_running( - &self, - job_id: &str, - item_id: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - status = ?, - assigned_thread_id = NULL, - attempt_count = attempt_count + 1, - updated_at = ?, - last_error = NULL -WHERE job_id = ? AND item_id = ? AND status = ? - "#, - ) - .bind(AgentJobItemStatus::Running.as_str()) - .bind(now) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Pending.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn mark_agent_job_item_running_with_thread( - &self, - job_id: &str, - item_id: &str, - thread_id: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - status = ?, - assigned_thread_id = ?, - attempt_count = attempt_count + 1, - updated_at = ?, - last_error = NULL -WHERE job_id = ? AND item_id = ? AND status = ? - "#, - ) - .bind(AgentJobItemStatus::Running.as_str()) - .bind(thread_id) - .bind(now) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Pending.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn mark_agent_job_item_pending( - &self, - job_id: &str, - item_id: &str, - error_message: Option<&str>, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - status = ?, - assigned_thread_id = NULL, - updated_at = ?, - last_error = ? -WHERE job_id = ? AND item_id = ? AND status = ? - "#, - ) - .bind(AgentJobItemStatus::Pending.as_str()) - .bind(now) - .bind(error_message) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Running.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn set_agent_job_item_thread( - &self, - job_id: &str, - item_id: &str, - thread_id: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET assigned_thread_id = ?, updated_at = ? -WHERE job_id = ? AND item_id = ? AND status = ? - "#, - ) - .bind(thread_id) - .bind(now) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Running.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn report_agent_job_item_result( - &self, - job_id: &str, - item_id: &str, - reporting_thread_id: &str, - result_json: &Value, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let serialized = serde_json::to_string(result_json)?; - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - result_json = ?, - reported_at = ?, - updated_at = ?, - last_error = NULL -WHERE - job_id = ? - AND item_id = ? - AND status = ? - AND assigned_thread_id = ? - "#, - ) - .bind(serialized) - .bind(now) - .bind(now) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Running.as_str()) - .bind(reporting_thread_id) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn mark_agent_job_item_completed( - &self, - job_id: &str, - item_id: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - status = ?, - completed_at = ?, - updated_at = ?, - assigned_thread_id = NULL -WHERE - job_id = ? - AND item_id = ? - AND status = ? - AND result_json IS NOT NULL - "#, - ) - .bind(AgentJobItemStatus::Completed.as_str()) - .bind(now) - .bind(now) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Running.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn mark_agent_job_item_failed( - &self, - job_id: &str, - item_id: &str, - error_message: &str, - ) -> anyhow::Result { - let now = Utc::now().timestamp(); - let result = sqlx::query( - r#" -UPDATE agent_job_items -SET - status = ?, - completed_at = ?, - updated_at = ?, - last_error = ?, - assigned_thread_id = NULL -WHERE - job_id = ? - AND item_id = ? - AND status = ? - "#, - ) - .bind(AgentJobItemStatus::Failed.as_str()) - .bind(now) - .bind(now) - .bind(error_message) - .bind(job_id) - .bind(item_id) - .bind(AgentJobItemStatus::Running.as_str()) - .execute(self.pool.as_ref()) - .await?; - Ok(result.rows_affected() > 0) - } - - pub async fn get_agent_job_progress(&self, job_id: &str) -> anyhow::Result { - let row = sqlx::query( - r#" -SELECT - COUNT(*) AS total_items, - SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS pending_items, - SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS running_items, - SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS completed_items, - SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS failed_items -FROM agent_job_items -WHERE job_id = ? - "#, - ) - .bind(AgentJobItemStatus::Pending.as_str()) - .bind(AgentJobItemStatus::Running.as_str()) - .bind(AgentJobItemStatus::Completed.as_str()) - .bind(AgentJobItemStatus::Failed.as_str()) - .bind(job_id) - .fetch_one(self.pool.as_ref()) - .await?; - - let total_items: i64 = row.try_get("total_items")?; - let pending_items: Option = row.try_get("pending_items")?; - let running_items: Option = row.try_get("running_items")?; - let completed_items: Option = row.try_get("completed_items")?; - let failed_items: Option = row.try_get("failed_items")?; - Ok(AgentJobProgress { - total_items: usize::try_from(total_items).unwrap_or_default(), - pending_items: usize::try_from(pending_items.unwrap_or_default()).unwrap_or_default(), - running_items: usize::try_from(running_items.unwrap_or_default()).unwrap_or_default(), - completed_items: usize::try_from(completed_items.unwrap_or_default()) - .unwrap_or_default(), - failed_items: usize::try_from(failed_items.unwrap_or_default()).unwrap_or_default(), - }) - } - - async fn ensure_backfill_state_row(&self) -> anyhow::Result<()> { - sqlx::query( - r#" -INSERT INTO backfill_state (id, status, last_watermark, last_success_at, updated_at) -VALUES (?, ?, NULL, NULL, ?) -ON CONFLICT(id) DO NOTHING - "#, - ) - .bind(1_i64) - .bind(crate::BackfillStatus::Pending.as_str()) - .bind(Utc::now().timestamp()) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } -} - -fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQuery) { - if let Some(level_upper) = query.level_upper.as_ref() { - builder - .push(" AND UPPER(level) = ") - .push_bind(level_upper.as_str()); - } - if let Some(from_ts) = query.from_ts { - builder.push(" AND ts >= ").push_bind(from_ts); - } - if let Some(to_ts) = query.to_ts { - builder.push(" AND ts <= ").push_bind(to_ts); - } - push_like_filters(builder, "module_path", &query.module_like); - push_like_filters(builder, "file", &query.file_like); - let has_thread_filter = !query.thread_ids.is_empty() || query.include_threadless; - if has_thread_filter { - builder.push(" AND ("); - let mut needs_or = false; - for thread_id in &query.thread_ids { - if needs_or { - builder.push(" OR "); - } - builder.push("thread_id = ").push_bind(thread_id.as_str()); - needs_or = true; - } - if query.include_threadless { - if needs_or { - builder.push(" OR "); - } - builder.push("thread_id IS NULL"); - } - builder.push(")"); - } - if let Some(after_id) = query.after_id { - builder.push(" AND id > ").push_bind(after_id); - } - if let Some(search) = query.search.as_ref() { - builder.push(" AND INSTR(message, "); - builder.push_bind(search.as_str()); - builder.push(") > 0"); - } -} - -fn push_like_filters<'a>( - builder: &mut QueryBuilder<'a, Sqlite>, - column: &str, - filters: &'a [String], -) { - if filters.is_empty() { - return; - } - builder.push(" AND ("); - for (idx, filter) in filters.iter().enumerate() { - if idx > 0 { - builder.push(" OR "); - } - builder - .push(column) - .push(" LIKE '%' || ") - .push_bind(filter.as_str()) - .push(" || '%'"); - } - builder.push(")"); -} - -fn extract_dynamic_tools(items: &[RolloutItem]) -> Option>> { - items.iter().find_map(|item| match item { - RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.dynamic_tools.clone()), - RolloutItem::ResponseItem(_) - | RolloutItem::Compacted(_) - | RolloutItem::TurnContext(_) - | RolloutItem::EventMsg(_) => None, - }) } async fn open_sqlite(path: &Path) -> anyhow::Result { @@ -1646,2149 +198,3 @@ fn should_remove_state_file(file_name: &str, current_name: &str) -> bool { }; !version_suffix.is_empty() && version_suffix.chars().all(|ch| ch.is_ascii_digit()) } - -fn push_thread_filters<'a>( - builder: &mut QueryBuilder<'a, Sqlite>, - archived_only: bool, - allowed_sources: &'a [String], - model_providers: Option<&'a [String]>, - anchor: Option<&crate::Anchor>, - sort_key: SortKey, -) { - builder.push(" WHERE 1 = 1"); - if archived_only { - builder.push(" AND archived = 1"); - } else { - builder.push(" AND archived = 0"); - } - builder.push(" AND first_user_message <> ''"); - if !allowed_sources.is_empty() { - builder.push(" AND source IN ("); - let mut separated = builder.separated(", "); - for source in allowed_sources { - separated.push_bind(source); - } - separated.push_unseparated(")"); - } - if let Some(model_providers) = model_providers - && !model_providers.is_empty() - { - builder.push(" AND model_provider IN ("); - let mut separated = builder.separated(", "); - for provider in model_providers { - separated.push_bind(provider); - } - separated.push_unseparated(")"); - } - if let Some(anchor) = anchor { - let anchor_ts = datetime_to_epoch_seconds(anchor.ts); - let column = match sort_key { - SortKey::CreatedAt => "created_at", - SortKey::UpdatedAt => "updated_at", - }; - builder.push(" AND ("); - builder.push(column); - builder.push(" < "); - builder.push_bind(anchor_ts); - builder.push(" OR ("); - builder.push(column); - builder.push(" = "); - builder.push_bind(anchor_ts); - builder.push(" AND id < "); - builder.push_bind(anchor.id.to_string()); - builder.push("))"); - } -} - -fn push_thread_order_and_limit( - builder: &mut QueryBuilder<'_, Sqlite>, - sort_key: SortKey, - limit: usize, -) { - let order_column = match sort_key { - SortKey::CreatedAt => "created_at", - SortKey::UpdatedAt => "updated_at", - }; - builder.push(" ORDER BY "); - builder.push(order_column); - builder.push(" DESC, id DESC"); - builder.push(" LIMIT "); - builder.push_bind(limit as i64); -} - -#[cfg(test)] -mod tests { - use super::StateRuntime; - use super::ThreadMetadata; - use super::state_db_filename; - use crate::LogEntry; - use crate::LogQuery; - use crate::STATE_DB_FILENAME; - use crate::STATE_DB_VERSION; - use crate::model::Phase2JobClaimOutcome; - use crate::model::Stage1JobClaimOutcome; - use crate::model::Stage1StartupClaimParams; - use chrono::DateTime; - use chrono::Duration; - use chrono::Utc; - use codex_protocol::ThreadId; - use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; - use pretty_assertions::assert_eq; - use sqlx::Row; - use std::path::Path; - use std::path::PathBuf; - use std::sync::Arc; - use std::time::SystemTime; - use std::time::UNIX_EPOCH; - use uuid::Uuid; - - fn unique_temp_dir() -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |duration| duration.as_nanos()); - std::env::temp_dir().join(format!( - "codex-state-runtime-test-{nanos}-{}", - Uuid::new_v4() - )) - } - - #[tokio::test] - async fn init_removes_legacy_state_db_files() { - let codex_home = unique_temp_dir(); - tokio::fs::create_dir_all(&codex_home) - .await - .expect("create codex_home"); - - let current_name = state_db_filename(); - let previous_version = STATE_DB_VERSION.saturating_sub(1); - let unversioned_name = format!("{STATE_DB_FILENAME}.sqlite"); - for suffix in ["", "-wal", "-shm", "-journal"] { - let path = codex_home.join(format!("{unversioned_name}{suffix}")); - tokio::fs::write(path, b"legacy") - .await - .expect("write legacy"); - let old_version_path = codex_home.join(format!( - "{STATE_DB_FILENAME}_{previous_version}.sqlite{suffix}" - )); - tokio::fs::write(old_version_path, b"old_version") - .await - .expect("write old version"); - } - let unrelated_path = codex_home.join("state.sqlite_backup"); - tokio::fs::write(&unrelated_path, b"keep") - .await - .expect("write unrelated"); - let numeric_path = codex_home.join("123"); - tokio::fs::write(&numeric_path, b"keep") - .await - .expect("write numeric"); - - let _runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - for suffix in ["", "-wal", "-shm", "-journal"] { - let legacy_path = codex_home.join(format!("{unversioned_name}{suffix}")); - assert_eq!( - tokio::fs::try_exists(&legacy_path) - .await - .expect("check legacy path"), - false - ); - let old_version_path = codex_home.join(format!( - "{STATE_DB_FILENAME}_{previous_version}.sqlite{suffix}" - )); - assert_eq!( - tokio::fs::try_exists(&old_version_path) - .await - .expect("check old version path"), - false - ); - } - assert_eq!( - tokio::fs::try_exists(codex_home.join(current_name)) - .await - .expect("check new db path"), - true - ); - assert_eq!( - tokio::fs::try_exists(&unrelated_path) - .await - .expect("check unrelated path"), - true - ); - assert_eq!( - tokio::fs::try_exists(&numeric_path) - .await - .expect("check numeric path"), - true - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn backfill_state_persists_progress_and_completion() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let initial = runtime - .get_backfill_state() - .await - .expect("get initial backfill state"); - assert_eq!(initial.status, crate::BackfillStatus::Pending); - assert_eq!(initial.last_watermark, None); - assert_eq!(initial.last_success_at, None); - - runtime - .mark_backfill_running() - .await - .expect("mark backfill running"); - runtime - .checkpoint_backfill("sessions/2026/01/27/rollout-a.jsonl") - .await - .expect("checkpoint backfill"); - - let running = runtime - .get_backfill_state() - .await - .expect("get running backfill state"); - assert_eq!(running.status, crate::BackfillStatus::Running); - assert_eq!( - running.last_watermark, - Some("sessions/2026/01/27/rollout-a.jsonl".to_string()) - ); - assert_eq!(running.last_success_at, None); - - runtime - .mark_backfill_complete(Some("sessions/2026/01/28/rollout-b.jsonl")) - .await - .expect("mark backfill complete"); - let completed = runtime - .get_backfill_state() - .await - .expect("get completed backfill state"); - assert_eq!(completed.status, crate::BackfillStatus::Complete); - assert_eq!( - completed.last_watermark, - Some("sessions/2026/01/28/rollout-b.jsonl".to_string()) - ); - assert!(completed.last_success_at.is_some()); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn backfill_claim_is_singleton_until_stale_and_blocked_when_complete() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let claimed = runtime - .try_claim_backfill(3600) - .await - .expect("initial backfill claim"); - assert_eq!(claimed, true); - - let duplicate_claim = runtime - .try_claim_backfill(3600) - .await - .expect("duplicate backfill claim"); - assert_eq!(duplicate_claim, false); - - let stale_updated_at = Utc::now().timestamp().saturating_sub(10_000); - sqlx::query( - r#" -UPDATE backfill_state -SET status = ?, updated_at = ? -WHERE id = 1 - "#, - ) - .bind(crate::BackfillStatus::Running.as_str()) - .bind(stale_updated_at) - .execute(runtime.pool.as_ref()) - .await - .expect("force stale backfill lease"); - - let stale_claim = runtime - .try_claim_backfill(10) - .await - .expect("stale backfill claim"); - assert_eq!(stale_claim, true); - - runtime - .mark_backfill_complete(None) - .await - .expect("mark complete"); - let claim_after_complete = runtime - .try_claim_backfill(3600) - .await - .expect("claim after complete"); - assert_eq!(claim_after_complete, false); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_claim_skips_when_up_to_date() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.join("a")); - runtime - .upsert_thread(&metadata) - .await - .expect("upsert thread"); - - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - - let claim = runtime - .try_claim_stage1_job(thread_id, owner_a, 100, 3600, 64) - .await - .expect("claim stage1 job"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected claim outcome: {other:?}"), - }; - - assert!( - runtime - .mark_stage1_job_succeeded( - thread_id, - ownership_token.as_str(), - 100, - "raw", - "sum", - None, - ) - .await - .expect("mark stage1 succeeded"), - "stage1 success should finalize for current token" - ); - - let up_to_date = runtime - .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) - .await - .expect("claim stage1 up-to-date"); - assert_eq!(up_to_date, Stage1JobClaimOutcome::SkippedUpToDate); - - let needs_rerun = runtime - .try_claim_stage1_job(thread_id, owner_b, 101, 3600, 64) - .await - .expect("claim stage1 newer source"); - assert!( - matches!(needs_rerun, Stage1JobClaimOutcome::Claimed { .. }), - "newer source_updated_at should be claimable" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_running_stale_can_be_stolen_but_fresh_running_is_skipped() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let cwd = codex_home.join("workspace"); - runtime - .upsert_thread(&test_thread_metadata(&codex_home, thread_id, cwd)) - .await - .expect("upsert thread"); - - let claim_a = runtime - .try_claim_stage1_job(thread_id, owner_a, 100, 3600, 64) - .await - .expect("claim a"); - assert!(matches!(claim_a, Stage1JobClaimOutcome::Claimed { .. })); - - let claim_b_fresh = runtime - .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) - .await - .expect("claim b fresh"); - assert_eq!(claim_b_fresh, Stage1JobClaimOutcome::SkippedRunning); - - sqlx::query("UPDATE jobs SET lease_until = 0 WHERE kind = 'memory_stage1' AND job_key = ?") - .bind(thread_id.to_string()) - .execute(runtime.pool.as_ref()) - .await - .expect("force stale lease"); - - let claim_b_stale = runtime - .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) - .await - .expect("claim b stale"); - assert!(matches!( - claim_b_stale, - Stage1JobClaimOutcome::Claimed { .. } - )); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_concurrent_claim_for_same_thread_is_conflict_safe() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id, - codex_home.join("workspace"), - )) - .await - .expect("upsert thread"); - - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let thread_id_a = thread_id; - let thread_id_b = thread_id; - let runtime_a = Arc::clone(&runtime); - let runtime_b = Arc::clone(&runtime); - let claim_with_retry = |runtime: Arc, - thread_id: ThreadId, - owner: ThreadId| async move { - for attempt in 0..5 { - match runtime - .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) - .await - { - Ok(outcome) => return outcome, - Err(err) if err.to_string().contains("database is locked") && attempt < 4 => { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - Err(err) => panic!("claim stage1 should not fail: {err}"), - } - } - panic!("claim stage1 should have returned within retry budget") - }; - - let (claim_a, claim_b) = tokio::join!( - claim_with_retry(runtime_a, thread_id_a, owner_a), - claim_with_retry(runtime_b, thread_id_b, owner_b), - ); - - let claim_outcomes = vec![claim_a, claim_b]; - let claimed_count = claim_outcomes - .iter() - .filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. })) - .count(); - assert_eq!(claimed_count, 1); - assert!( - claim_outcomes.iter().all(|outcome| { - matches!( - outcome, - Stage1JobClaimOutcome::Claimed { .. } | Stage1JobClaimOutcome::SkippedRunning - ) - }), - "unexpected claim outcomes: {claim_outcomes:?}" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_concurrent_claims_respect_running_cap() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_a, - codex_home.join("workspace-a"), - )) - .await - .expect("upsert thread a"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_b, - codex_home.join("workspace-b"), - )) - .await - .expect("upsert thread b"); - - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let runtime_a = Arc::clone(&runtime); - let runtime_b = Arc::clone(&runtime); - - let (claim_a, claim_b) = tokio::join!( - async move { - runtime_a - .try_claim_stage1_job(thread_a, owner_a, 100, 3_600, 1) - .await - .expect("claim stage1 thread a") - }, - async move { - runtime_b - .try_claim_stage1_job(thread_b, owner_b, 101, 3_600, 1) - .await - .expect("claim stage1 thread b") - }, - ); - - let claim_outcomes = vec![claim_a, claim_b]; - let claimed_count = claim_outcomes - .iter() - .filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. })) - .count(); - assert_eq!(claimed_count, 1); - assert!( - claim_outcomes - .iter() - .any(|outcome| { matches!(outcome, Stage1JobClaimOutcome::SkippedRunning) }), - "one concurrent claim should be throttled by running cap: {claim_outcomes:?}" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn claim_stage1_jobs_filters_by_age_idle_and_current_thread() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let now = Utc::now(); - let fresh_at = now - Duration::hours(1); - let just_under_idle_at = now - Duration::hours(12) + Duration::minutes(1); - let eligible_idle_at = now - Duration::hours(12) - Duration::minutes(1); - let old_at = now - Duration::days(31); - - let current_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); - let fresh_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("fresh thread id"); - let just_under_idle_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("just under idle thread id"); - let eligible_idle_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("eligible idle thread id"); - let old_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("old thread id"); - - let mut current = - test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); - current.created_at = now; - current.updated_at = now; - runtime - .upsert_thread(¤t) - .await - .expect("upsert current"); - - let mut fresh = - test_thread_metadata(&codex_home, fresh_thread_id, codex_home.join("fresh")); - fresh.created_at = fresh_at; - fresh.updated_at = fresh_at; - runtime.upsert_thread(&fresh).await.expect("upsert fresh"); - - let mut just_under_idle = test_thread_metadata( - &codex_home, - just_under_idle_thread_id, - codex_home.join("just-under-idle"), - ); - just_under_idle.created_at = just_under_idle_at; - just_under_idle.updated_at = just_under_idle_at; - runtime - .upsert_thread(&just_under_idle) - .await - .expect("upsert just-under-idle"); - - let mut eligible_idle = test_thread_metadata( - &codex_home, - eligible_idle_thread_id, - codex_home.join("eligible-idle"), - ); - eligible_idle.created_at = eligible_idle_at; - eligible_idle.updated_at = eligible_idle_at; - runtime - .upsert_thread(&eligible_idle) - .await - .expect("upsert eligible-idle"); - - let mut old = test_thread_metadata(&codex_home, old_thread_id, codex_home.join("old")); - old.created_at = old_at; - old.updated_at = old_at; - runtime.upsert_thread(&old).await.expect("upsert old"); - - let allowed_sources = vec!["cli".to_string()]; - let claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 1, - max_claimed: 5, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3600, - }, - ) - .await - .expect("claim stage1 jobs"); - - assert_eq!(claims.len(), 1); - assert_eq!(claims[0].thread.id, eligible_idle_thread_id); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn claim_stage1_jobs_prefilters_threads_with_up_to_date_memory() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let now = Utc::now(); - let eligible_newer_at = now - Duration::hours(13); - let eligible_older_at = now - Duration::hours(14); - - let current_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); - let up_to_date_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("up-to-date thread id"); - let stale_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("stale thread id"); - let worker_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("worker id"); - - let mut current = - test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); - current.created_at = now; - current.updated_at = now; - runtime - .upsert_thread(¤t) - .await - .expect("upsert current thread"); - - let mut up_to_date = test_thread_metadata( - &codex_home, - up_to_date_thread_id, - codex_home.join("up-to-date"), - ); - up_to_date.created_at = eligible_newer_at; - up_to_date.updated_at = eligible_newer_at; - runtime - .upsert_thread(&up_to_date) - .await - .expect("upsert up-to-date thread"); - - let up_to_date_claim = runtime - .try_claim_stage1_job( - up_to_date_thread_id, - worker_id, - up_to_date.updated_at.timestamp(), - 3600, - 64, - ) - .await - .expect("claim up-to-date thread for seed"); - let up_to_date_token = match up_to_date_claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected seed claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - up_to_date_thread_id, - up_to_date_token.as_str(), - up_to_date.updated_at.timestamp(), - "raw", - "summary", - None, - ) - .await - .expect("mark up-to-date thread succeeded"), - "seed stage1 success should complete for up-to-date thread" - ); - - let mut stale = - test_thread_metadata(&codex_home, stale_thread_id, codex_home.join("stale")); - stale.created_at = eligible_older_at; - stale.updated_at = eligible_older_at; - runtime - .upsert_thread(&stale) - .await - .expect("upsert stale thread"); - - let allowed_sources = vec!["cli".to_string()]; - let claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 1, - max_claimed: 1, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3600, - }, - ) - .await - .expect("claim stage1 startup jobs"); - assert_eq!(claims.len(), 1); - assert_eq!(claims[0].thread.id, stale_thread_id); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn claim_stage1_jobs_enforces_global_running_cap() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let current_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - current_thread_id, - codex_home.join("current"), - )) - .await - .expect("upsert current"); - - let now = Utc::now(); - let started_at = now.timestamp(); - let lease_until = started_at + 3600; - let eligible_at = now - Duration::hours(13); - let existing_running = 10usize; - let total_candidates = 80usize; - - for idx in 0..total_candidates { - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let mut metadata = test_thread_metadata( - &codex_home, - thread_id, - codex_home.join(format!("thread-{idx}")), - ); - metadata.created_at = eligible_at - Duration::seconds(idx as i64); - metadata.updated_at = eligible_at - Duration::seconds(idx as i64); - runtime - .upsert_thread(&metadata) - .await - .expect("upsert thread"); - - if idx < existing_running { - sqlx::query( - r#" -INSERT INTO jobs ( - kind, - job_key, - status, - worker_id, - ownership_token, - started_at, - finished_at, - lease_until, - retry_at, - retry_remaining, - last_error, - input_watermark, - last_success_watermark -) VALUES (?, ?, 'running', ?, ?, ?, NULL, ?, NULL, ?, NULL, ?, NULL) - "#, - ) - .bind("memory_stage1") - .bind(thread_id.to_string()) - .bind(current_thread_id.to_string()) - .bind(Uuid::new_v4().to_string()) - .bind(started_at) - .bind(lease_until) - .bind(3) - .bind(metadata.updated_at.timestamp()) - .execute(runtime.pool.as_ref()) - .await - .expect("seed running stage1 job"); - } - } - - let allowed_sources = vec!["cli".to_string()]; - let claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 200, - max_claimed: 64, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3600, - }, - ) - .await - .expect("claim stage1 jobs"); - assert_eq!(claims.len(), 54); - - let running_count = sqlx::query( - r#" -SELECT COUNT(*) AS count -FROM jobs -WHERE kind = 'memory_stage1' - AND status = 'running' - AND lease_until IS NOT NULL - AND lease_until > ? - "#, - ) - .bind(Utc::now().timestamp()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("count running stage1 jobs") - .try_get::("count") - .expect("running count value"); - assert_eq!(running_count, 64); - - let more_claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 200, - max_claimed: 64, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3600, - }, - ) - .await - .expect("claim stage1 jobs with cap reached"); - assert_eq!(more_claims.len(), 0); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn claim_stage1_jobs_processes_two_full_batches_across_startup_passes() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let current_thread_id = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); - let mut current = - test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); - current.created_at = Utc::now(); - current.updated_at = Utc::now(); - runtime - .upsert_thread(¤t) - .await - .expect("upsert current"); - - let eligible_at = Utc::now() - Duration::hours(13); - for idx in 0..200 { - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let mut metadata = test_thread_metadata( - &codex_home, - thread_id, - codex_home.join(format!("thread-{idx}")), - ); - metadata.created_at = eligible_at - Duration::seconds(idx as i64); - metadata.updated_at = eligible_at - Duration::seconds(idx as i64); - runtime - .upsert_thread(&metadata) - .await - .expect("upsert eligible thread"); - } - - let allowed_sources = vec!["cli".to_string()]; - let first_claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 5_000, - max_claimed: 64, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3_600, - }, - ) - .await - .expect("first stage1 startup claim"); - assert_eq!(first_claims.len(), 64); - - for claim in first_claims { - assert!( - runtime - .mark_stage1_job_succeeded( - claim.thread.id, - claim.ownership_token.as_str(), - claim.thread.updated_at.timestamp(), - "raw", - "summary", - None, - ) - .await - .expect("mark first-batch stage1 success"), - "first batch stage1 completion should succeed" - ); - } - - let second_claims = runtime - .claim_stage1_jobs_for_startup( - current_thread_id, - Stage1StartupClaimParams { - scan_limit: 5_000, - max_claimed: 64, - max_age_days: 30, - min_rollout_idle_hours: 12, - allowed_sources: allowed_sources.as_slice(), - lease_seconds: 3_600, - }, - ) - .await - .expect("second stage1 startup claim"); - assert_eq!(second_claims.len(), 64); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_output_cascades_on_thread_delete() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let cwd = codex_home.join("workspace"); - runtime - .upsert_thread(&test_thread_metadata(&codex_home, thread_id, cwd)) - .await - .expect("upsert thread"); - - let claim = runtime - .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) - .await - .expect("claim stage1"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - thread_id, - ownership_token.as_str(), - 100, - "raw", - "sum", - None, - ) - .await - .expect("mark stage1 succeeded"), - "mark stage1 succeeded should write stage1_outputs" - ); - - let count_before = - sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") - .bind(thread_id.to_string()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("count before delete") - .try_get::("count") - .expect("count value"); - assert_eq!(count_before, 1); - - sqlx::query("DELETE FROM threads WHERE id = ?") - .bind(thread_id.to_string()) - .execute(runtime.pool.as_ref()) - .await - .expect("delete thread"); - - let count_after = - sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") - .bind(thread_id.to_string()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("count after delete") - .try_get::("count") - .expect("count value"); - assert_eq!(count_after, 0); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn mark_stage1_job_succeeded_no_output_skips_phase2_when_output_was_already_absent() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id, - codex_home.join("workspace"), - )) - .await - .expect("upsert thread"); - - let claim = runtime - .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) - .await - .expect("claim stage1"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded_no_output(thread_id, ownership_token.as_str()) - .await - .expect("mark stage1 succeeded without output"), - "stage1 no-output success should complete the job" - ); - - let output_row_count = - sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") - .bind(thread_id.to_string()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("load stage1 output count") - .try_get::("count") - .expect("stage1 output count"); - assert_eq!( - output_row_count, 0, - "stage1 no-output success should not persist empty stage1 outputs" - ); - - let up_to_date = runtime - .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) - .await - .expect("claim stage1 up-to-date"); - assert_eq!(up_to_date, Stage1JobClaimOutcome::SkippedUpToDate); - - let global_job_row_count = sqlx::query("SELECT COUNT(*) AS count FROM jobs WHERE kind = ?") - .bind("memory_consolidate_global") - .fetch_one(runtime.pool.as_ref()) - .await - .expect("load phase2 job row count") - .try_get::("count") - .expect("phase2 job row count"); - assert_eq!( - global_job_row_count, 0, - "no-output without an existing stage1 output should not enqueue phase2" - ); - - let claim_phase2 = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2"); - assert_eq!( - claim_phase2, - Phase2JobClaimOutcome::SkippedNotDirty, - "phase2 should remain clean when no-output deleted nothing" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn mark_stage1_job_succeeded_no_output_enqueues_phase2_when_deleting_output() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id, - codex_home.join("workspace"), - )) - .await - .expect("upsert thread"); - - let first_claim = runtime - .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) - .await - .expect("claim initial stage1"); - let first_token = match first_claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected initial stage1 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded(thread_id, first_token.as_str(), 100, "raw", "sum", None) - .await - .expect("mark initial stage1 succeeded"), - "initial stage1 success should create stage1 output" - ); - - let phase2_claim = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2 after initial output"); - let (phase2_token, phase2_input_watermark) = match phase2_claim { - Phase2JobClaimOutcome::Claimed { - ownership_token, - input_watermark, - } => (ownership_token, input_watermark), - other => panic!("unexpected phase2 claim after initial output: {other:?}"), - }; - assert_eq!(phase2_input_watermark, 100); - assert!( - runtime - .mark_global_phase2_job_succeeded(phase2_token.as_str(), phase2_input_watermark) - .await - .expect("mark initial phase2 succeeded"), - "initial phase2 success should clear global dirty state" - ); - - let no_output_claim = runtime - .try_claim_stage1_job(thread_id, owner_b, 101, 3600, 64) - .await - .expect("claim stage1 for no-output delete"); - let no_output_token = match no_output_claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected no-output stage1 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded_no_output(thread_id, no_output_token.as_str()) - .await - .expect("mark stage1 no-output after existing output"), - "no-output should succeed when deleting an existing stage1 output" - ); - - let output_row_count = - sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") - .bind(thread_id.to_string()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("load stage1 output count after delete") - .try_get::("count") - .expect("stage1 output count"); - assert_eq!(output_row_count, 0); - - let claim_phase2 = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2 after no-output deletion"); - let (phase2_token, phase2_input_watermark) = match claim_phase2 { - Phase2JobClaimOutcome::Claimed { - ownership_token, - input_watermark, - } => (ownership_token, input_watermark), - other => panic!("unexpected phase2 claim after no-output deletion: {other:?}"), - }; - assert_eq!(phase2_input_watermark, 101); - assert!( - runtime - .mark_global_phase2_job_succeeded(phase2_token.as_str(), phase2_input_watermark) - .await - .expect("mark phase2 succeeded after no-output delete") - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn stage1_retry_exhaustion_does_not_block_newer_watermark() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id, - codex_home.join("workspace"), - )) - .await - .expect("upsert thread"); - - for attempt in 0..3 { - let claim = runtime - .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) - .await - .expect("claim stage1 for retry exhaustion"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!( - "attempt {} should claim stage1 before retries are exhausted: {other:?}", - attempt + 1 - ), - }; - assert!( - runtime - .mark_stage1_job_failed(thread_id, ownership_token.as_str(), "boom", 0) - .await - .expect("mark stage1 failed"), - "attempt {} should decrement retry budget", - attempt + 1 - ); - } - - let exhausted_claim = runtime - .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) - .await - .expect("claim stage1 after retry exhaustion"); - assert_eq!( - exhausted_claim, - Stage1JobClaimOutcome::SkippedRetryExhausted - ); - - let newer_source_claim = runtime - .try_claim_stage1_job(thread_id, owner, 101, 3_600, 64) - .await - .expect("claim stage1 with newer source watermark"); - assert!( - matches!(newer_source_claim, Stage1JobClaimOutcome::Claimed { .. }), - "newer source watermark should reset retry budget and be claimable" - ); - - let job_row = sqlx::query( - "SELECT retry_remaining, input_watermark FROM jobs WHERE kind = ? AND job_key = ?", - ) - .bind("memory_stage1") - .bind(thread_id.to_string()) - .fetch_one(runtime.pool.as_ref()) - .await - .expect("load stage1 job row after newer-source claim"); - assert_eq!( - job_row - .try_get::("retry_remaining") - .expect("retry_remaining"), - 3 - ); - assert_eq!( - job_row - .try_get::("input_watermark") - .expect("input_watermark"), - 101 - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn phase2_global_consolidation_reruns_when_watermark_advances() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - - runtime - .enqueue_global_consolidation(100) - .await - .expect("enqueue global consolidation"); - - let claim = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2"); - let (ownership_token, input_watermark) = match claim { - Phase2JobClaimOutcome::Claimed { - ownership_token, - input_watermark, - } => (ownership_token, input_watermark), - other => panic!("unexpected phase2 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_global_phase2_job_succeeded(ownership_token.as_str(), input_watermark) - .await - .expect("mark phase2 succeeded"), - "phase2 success should finalize for current token" - ); - - let claim_up_to_date = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2 up-to-date"); - assert_eq!(claim_up_to_date, Phase2JobClaimOutcome::SkippedNotDirty); - - runtime - .enqueue_global_consolidation(101) - .await - .expect("enqueue global consolidation again"); - - let claim_rerun = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim phase2 rerun"); - assert!( - matches!(claim_rerun, Phase2JobClaimOutcome::Claimed { .. }), - "advanced watermark should be claimable" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn list_stage1_outputs_for_global_returns_latest_outputs() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id_a, - codex_home.join("workspace-a"), - )) - .await - .expect("upsert thread a"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id_b, - codex_home.join("workspace-b"), - )) - .await - .expect("upsert thread b"); - - let claim = runtime - .try_claim_stage1_job(thread_id_a, owner, 100, 3600, 64) - .await - .expect("claim stage1 a"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected stage1 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - thread_id_a, - ownership_token.as_str(), - 100, - "raw memory a", - "summary a", - None, - ) - .await - .expect("mark stage1 succeeded a"), - "stage1 success should persist output a" - ); - - let claim = runtime - .try_claim_stage1_job(thread_id_b, owner, 101, 3600, 64) - .await - .expect("claim stage1 b"); - let ownership_token = match claim { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected stage1 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - thread_id_b, - ownership_token.as_str(), - 101, - "raw memory b", - "summary b", - Some("rollout-b"), - ) - .await - .expect("mark stage1 succeeded b"), - "stage1 success should persist output b" - ); - - let outputs = runtime - .list_stage1_outputs_for_global(10) - .await - .expect("list stage1 outputs for global"); - assert_eq!(outputs.len(), 2); - assert_eq!(outputs[0].thread_id, thread_id_b); - assert_eq!(outputs[0].rollout_summary, "summary b"); - assert_eq!(outputs[0].rollout_slug.as_deref(), Some("rollout-b")); - assert_eq!(outputs[0].cwd, codex_home.join("workspace-b")); - assert_eq!(outputs[1].thread_id, thread_id_a); - assert_eq!(outputs[1].rollout_summary, "summary a"); - assert_eq!(outputs[1].rollout_slug, None); - assert_eq!(outputs[1].cwd, codex_home.join("workspace-a")); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn list_stage1_outputs_for_global_skips_empty_payloads() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_id_non_empty = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - let thread_id_empty = - ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id_non_empty, - codex_home.join("workspace-non-empty"), - )) - .await - .expect("upsert non-empty thread"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_id_empty, - codex_home.join("workspace-empty"), - )) - .await - .expect("upsert empty thread"); - - sqlx::query( - r#" -INSERT INTO stage1_outputs (thread_id, source_updated_at, raw_memory, rollout_summary, generated_at) -VALUES (?, ?, ?, ?, ?) - "#, - ) - .bind(thread_id_non_empty.to_string()) - .bind(100_i64) - .bind("raw memory") - .bind("summary") - .bind(100_i64) - .execute(runtime.pool.as_ref()) - .await - .expect("insert non-empty stage1 output"); - sqlx::query( - r#" -INSERT INTO stage1_outputs (thread_id, source_updated_at, raw_memory, rollout_summary, generated_at) -VALUES (?, ?, ?, ?, ?) - "#, - ) - .bind(thread_id_empty.to_string()) - .bind(101_i64) - .bind("") - .bind("") - .bind(101_i64) - .execute(runtime.pool.as_ref()) - .await - .expect("insert empty stage1 output"); - - let outputs = runtime - .list_stage1_outputs_for_global(1) - .await - .expect("list stage1 outputs for global"); - assert_eq!(outputs.len(), 1); - assert_eq!(outputs[0].thread_id, thread_id_non_empty); - assert_eq!(outputs[0].rollout_summary, "summary"); - assert_eq!(outputs[0].cwd, codex_home.join("workspace-non-empty")); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn mark_stage1_job_succeeded_enqueues_global_consolidation() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a"); - let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b"); - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); - - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_a, - codex_home.join("workspace-a"), - )) - .await - .expect("upsert thread a"); - runtime - .upsert_thread(&test_thread_metadata( - &codex_home, - thread_b, - codex_home.join("workspace-b"), - )) - .await - .expect("upsert thread b"); - - let claim_a = runtime - .try_claim_stage1_job(thread_a, owner, 100, 3600, 64) - .await - .expect("claim stage1 a"); - let token_a = match claim_a { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected stage1 claim outcome for thread a: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - thread_a, - token_a.as_str(), - 100, - "raw-a", - "summary-a", - None, - ) - .await - .expect("mark stage1 succeeded a"), - "stage1 success should persist output for thread a" - ); - - let claim_b = runtime - .try_claim_stage1_job(thread_b, owner, 101, 3600, 64) - .await - .expect("claim stage1 b"); - let token_b = match claim_b { - Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, - other => panic!("unexpected stage1 claim outcome for thread b: {other:?}"), - }; - assert!( - runtime - .mark_stage1_job_succeeded( - thread_b, - token_b.as_str(), - 101, - "raw-b", - "summary-b", - None, - ) - .await - .expect("mark stage1 succeeded b"), - "stage1 success should persist output for thread b" - ); - - let claim = runtime - .try_claim_global_phase2_job(owner, 3600) - .await - .expect("claim global consolidation"); - let input_watermark = match claim { - Phase2JobClaimOutcome::Claimed { - input_watermark, .. - } => input_watermark, - other => panic!("unexpected global consolidation claim outcome: {other:?}"), - }; - assert_eq!(input_watermark, 101); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn phase2_global_lock_allows_only_one_fresh_runner() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - runtime - .enqueue_global_consolidation(200) - .await - .expect("enqueue global consolidation"); - - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); - - let running_claim = runtime - .try_claim_global_phase2_job(owner_a, 3600) - .await - .expect("claim global lock"); - assert!( - matches!(running_claim, Phase2JobClaimOutcome::Claimed { .. }), - "first owner should claim global lock" - ); - - let second_claim = runtime - .try_claim_global_phase2_job(owner_b, 3600) - .await - .expect("claim global lock from second owner"); - assert_eq!(second_claim, Phase2JobClaimOutcome::SkippedRunning); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn phase2_global_lock_stale_lease_allows_takeover() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - runtime - .enqueue_global_consolidation(300) - .await - .expect("enqueue global consolidation"); - - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); - - let initial_claim = runtime - .try_claim_global_phase2_job(owner_a, 3600) - .await - .expect("claim initial global lock"); - let token_a = match initial_claim { - Phase2JobClaimOutcome::Claimed { - ownership_token, .. - } => ownership_token, - other => panic!("unexpected initial claim outcome: {other:?}"), - }; - - sqlx::query("UPDATE jobs SET lease_until = ? WHERE kind = ? AND job_key = ?") - .bind(Utc::now().timestamp() - 1) - .bind("memory_consolidate_global") - .bind("global") - .execute(runtime.pool.as_ref()) - .await - .expect("expire global consolidation lease"); - - let takeover_claim = runtime - .try_claim_global_phase2_job(owner_b, 3600) - .await - .expect("claim stale global lock"); - let (token_b, input_watermark) = match takeover_claim { - Phase2JobClaimOutcome::Claimed { - ownership_token, - input_watermark, - } => (ownership_token, input_watermark), - other => panic!("unexpected takeover claim outcome: {other:?}"), - }; - assert_ne!(token_a, token_b); - assert_eq!(input_watermark, 300); - - assert_eq!( - runtime - .mark_global_phase2_job_succeeded(token_a.as_str(), 300) - .await - .expect("mark stale owner success result"), - false, - "stale owner should lose finalization ownership after takeover" - ); - assert!( - runtime - .mark_global_phase2_job_succeeded(token_b.as_str(), 300) - .await - .expect("mark takeover owner success"), - "takeover owner should finalize consolidation" - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn phase2_backfilled_inputs_below_last_success_still_become_dirty() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - runtime - .enqueue_global_consolidation(500) - .await - .expect("enqueue initial consolidation"); - let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); - let claim_a = runtime - .try_claim_global_phase2_job(owner_a, 3_600) - .await - .expect("claim initial consolidation"); - let token_a = match claim_a { - Phase2JobClaimOutcome::Claimed { - ownership_token, - input_watermark, - } => { - assert_eq!(input_watermark, 500); - ownership_token - } - other => panic!("unexpected initial phase2 claim outcome: {other:?}"), - }; - assert!( - runtime - .mark_global_phase2_job_succeeded(token_a.as_str(), 500) - .await - .expect("mark initial phase2 success"), - "initial phase2 success should finalize" - ); - - runtime - .enqueue_global_consolidation(400) - .await - .expect("enqueue backfilled consolidation"); - - let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); - let claim_b = runtime - .try_claim_global_phase2_job(owner_b, 3_600) - .await - .expect("claim backfilled consolidation"); - match claim_b { - Phase2JobClaimOutcome::Claimed { - input_watermark, .. - } => { - assert!( - input_watermark > 500, - "backfilled enqueue should advance dirty watermark beyond last success" - ); - } - other => panic!("unexpected backfilled phase2 claim outcome: {other:?}"), - } - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn phase2_failure_fallback_updates_unowned_running_job() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - runtime - .enqueue_global_consolidation(400) - .await - .expect("enqueue global consolidation"); - - let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner"); - let claim = runtime - .try_claim_global_phase2_job(owner, 3_600) - .await - .expect("claim global consolidation"); - let ownership_token = match claim { - Phase2JobClaimOutcome::Claimed { - ownership_token, .. - } => ownership_token, - other => panic!("unexpected claim outcome: {other:?}"), - }; - - sqlx::query("UPDATE jobs SET ownership_token = NULL WHERE kind = ? AND job_key = ?") - .bind("memory_consolidate_global") - .bind("global") - .execute(runtime.pool.as_ref()) - .await - .expect("clear ownership token"); - - assert_eq!( - runtime - .mark_global_phase2_job_failed(ownership_token.as_str(), "lost", 3_600) - .await - .expect("mark phase2 failed with strict ownership"), - false, - "strict failure update should not match unowned running job" - ); - assert!( - runtime - .mark_global_phase2_job_failed_if_unowned(ownership_token.as_str(), "lost", 3_600) - .await - .expect("fallback failure update should match unowned running job"), - "fallback failure update should transition the unowned running job" - ); - - let claim = runtime - .try_claim_global_phase2_job(ThreadId::new(), 3_600) - .await - .expect("claim after fallback failure"); - assert_eq!(claim, Phase2JobClaimOutcome::SkippedNotDirty); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn query_logs_with_search_matches_substring() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - runtime - .insert_logs(&[ - LogEntry { - ts: 1_700_000_001, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some("alpha".to_string()), - thread_id: Some("thread-1".to_string()), - process_uuid: None, - file: Some("main.rs".to_string()), - line: Some(42), - module_path: None, - }, - LogEntry { - ts: 1_700_000_002, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some("alphabet".to_string()), - thread_id: Some("thread-1".to_string()), - process_uuid: None, - file: Some("main.rs".to_string()), - line: Some(43), - module_path: None, - }, - ]) - .await - .expect("insert test logs"); - - let rows = runtime - .query_logs(&LogQuery { - search: Some("alphab".to_string()), - ..Default::default() - }) - .await - .expect("query matching logs"); - - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].message.as_deref(), Some("alphabet")); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_old_rows_when_thread_exceeds_size_limit() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let six_mebibytes = "a".repeat(6 * 1024 * 1024); - runtime - .insert_logs(&[ - LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes.clone()), - thread_id: Some("thread-1".to_string()), - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }, - LogEntry { - ts: 2, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes.clone()), - thread_id: Some("thread-1".to_string()), - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(2), - module_path: Some("mod".to_string()), - }, - ]) - .await - .expect("insert test logs"); - - let rows = runtime - .query_logs(&LogQuery { - thread_ids: vec!["thread-1".to_string()], - ..Default::default() - }) - .await - .expect("query thread logs"); - - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].ts, 2); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let eleven_mebibytes = "d".repeat(11 * 1024 * 1024); - runtime - .insert_logs(&[LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(eleven_mebibytes), - thread_id: Some("thread-oversized".to_string()), - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }]) - .await - .expect("insert test log"); - - let rows = runtime - .query_logs(&LogQuery { - thread_ids: vec!["thread-oversized".to_string()], - ..Default::default() - }) - .await - .expect("query thread logs"); - - assert!(rows.is_empty()); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_threadless_rows_per_process_uuid_only() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let six_mebibytes = "b".repeat(6 * 1024 * 1024); - runtime - .insert_logs(&[ - LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes.clone()), - thread_id: None, - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }, - LogEntry { - ts: 2, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes.clone()), - thread_id: None, - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(2), - module_path: Some("mod".to_string()), - }, - LogEntry { - ts: 3, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes), - thread_id: Some("thread-1".to_string()), - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(3), - module_path: Some("mod".to_string()), - }, - ]) - .await - .expect("insert test logs"); - - let rows = runtime - .query_logs(&LogQuery { - thread_ids: vec!["thread-1".to_string()], - include_threadless: true, - ..Default::default() - }) - .await - .expect("query thread and threadless logs"); - - let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); - timestamps.sort_unstable(); - assert_eq!(timestamps, vec![2, 3]); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let eleven_mebibytes = "e".repeat(11 * 1024 * 1024); - runtime - .insert_logs(&[LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(eleven_mebibytes), - thread_id: None, - process_uuid: Some("proc-oversized".to_string()), - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }]) - .await - .expect("insert test log"); - - let rows = runtime - .query_logs(&LogQuery { - include_threadless: true, - ..Default::default() - }) - .await - .expect("query threadless logs"); - - assert!(rows.is_empty()); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_threadless_rows_with_null_process_uuid() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let six_mebibytes = "c".repeat(6 * 1024 * 1024); - runtime - .insert_logs(&[ - LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes.clone()), - thread_id: None, - process_uuid: None, - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }, - LogEntry { - ts: 2, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(six_mebibytes), - thread_id: None, - process_uuid: None, - file: Some("main.rs".to_string()), - line: Some(2), - module_path: Some("mod".to_string()), - }, - LogEntry { - ts: 3, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some("small".to_string()), - thread_id: None, - process_uuid: Some("proc-1".to_string()), - file: Some("main.rs".to_string()), - line: Some(3), - module_path: Some("mod".to_string()), - }, - ]) - .await - .expect("insert test logs"); - - let rows = runtime - .query_logs(&LogQuery { - include_threadless: true, - ..Default::default() - }) - .await - .expect("query threadless logs"); - - let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); - timestamps.sort_unstable(); - assert_eq!(timestamps, vec![2, 3]); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - #[tokio::test] - async fn insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) - .await - .expect("initialize runtime"); - - let eleven_mebibytes = "f".repeat(11 * 1024 * 1024); - runtime - .insert_logs(&[LogEntry { - ts: 1, - ts_nanos: 0, - level: "INFO".to_string(), - target: "cli".to_string(), - message: Some(eleven_mebibytes), - thread_id: None, - process_uuid: None, - file: Some("main.rs".to_string()), - line: Some(1), - module_path: Some("mod".to_string()), - }]) - .await - .expect("insert test log"); - - let rows = runtime - .query_logs(&LogQuery { - include_threadless: true, - ..Default::default() - }) - .await - .expect("query threadless logs"); - - assert!(rows.is_empty()); - - let _ = tokio::fs::remove_dir_all(codex_home).await; - } - - fn test_thread_metadata( - codex_home: &Path, - thread_id: ThreadId, - cwd: PathBuf, - ) -> ThreadMetadata { - let now = DateTime::::from_timestamp(1_700_000_000, 0).expect("timestamp"); - ThreadMetadata { - id: thread_id, - rollout_path: codex_home.join(format!("rollout-{thread_id}.jsonl")), - created_at: now, - updated_at: now, - source: "cli".to_string(), - agent_nickname: None, - agent_role: None, - model_provider: "test-provider".to_string(), - cwd, - cli_version: "0.0.0".to_string(), - title: String::new(), - sandbox_policy: crate::extract::enum_to_string(&SandboxPolicy::new_read_only_policy()), - approval_mode: crate::extract::enum_to_string(&AskForApproval::OnRequest), - tokens_used: 0, - first_user_message: Some("hello".to_string()), - archived_at: None, - git_sha: None, - git_branch: None, - git_origin_url: None, - } - } -} diff --git a/codex-rs/state/src/runtime/agent_jobs.rs b/codex-rs/state/src/runtime/agent_jobs.rs new file mode 100644 index 0000000000..c685605945 --- /dev/null +++ b/codex-rs/state/src/runtime/agent_jobs.rs @@ -0,0 +1,562 @@ +use super::*; +use crate::model::AgentJobItemRow; + +impl StateRuntime { + pub async fn create_agent_job( + &self, + params: &AgentJobCreateParams, + items: &[AgentJobItemCreateParams], + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let input_headers_json = serde_json::to_string(¶ms.input_headers)?; + let output_schema_json = params + .output_schema_json + .as_ref() + .map(serde_json::to_string) + .transpose()?; + let max_runtime_seconds = params + .max_runtime_seconds + .map(i64::try_from) + .transpose() + .map_err(|_| anyhow::anyhow!("invalid max_runtime_seconds value"))?; + let mut tx = self.pool.begin().await?; + sqlx::query( + r#" +INSERT INTO agent_jobs ( + id, + name, + status, + instruction, + auto_export, + max_runtime_seconds, + output_schema_json, + input_headers_json, + input_csv_path, + output_csv_path, + created_at, + updated_at, + started_at, + completed_at, + last_error +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL) + "#, + ) + .bind(params.id.as_str()) + .bind(params.name.as_str()) + .bind(AgentJobStatus::Pending.as_str()) + .bind(params.instruction.as_str()) + .bind(i64::from(params.auto_export)) + .bind(max_runtime_seconds) + .bind(output_schema_json) + .bind(input_headers_json) + .bind(params.input_csv_path.as_str()) + .bind(params.output_csv_path.as_str()) + .bind(now) + .bind(now) + .execute(&mut *tx) + .await?; + + for item in items { + let row_json = serde_json::to_string(&item.row_json)?; + sqlx::query( + r#" +INSERT INTO agent_job_items ( + job_id, + item_id, + row_index, + source_id, + row_json, + status, + assigned_thread_id, + attempt_count, + result_json, + last_error, + created_at, + updated_at, + completed_at, + reported_at +) VALUES (?, ?, ?, ?, ?, ?, NULL, 0, NULL, NULL, ?, ?, NULL, NULL) + "#, + ) + .bind(params.id.as_str()) + .bind(item.item_id.as_str()) + .bind(item.row_index) + .bind(item.source_id.as_deref()) + .bind(row_json) + .bind(AgentJobItemStatus::Pending.as_str()) + .bind(now) + .bind(now) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + + let job_id = params.id.as_str(); + self.get_agent_job(job_id) + .await? + .ok_or_else(|| anyhow::anyhow!("failed to load created agent job {job_id}")) + } + + pub async fn get_agent_job(&self, job_id: &str) -> anyhow::Result> { + let row = sqlx::query_as::<_, AgentJobRow>( + r#" +SELECT + id, + name, + status, + instruction, + auto_export, + max_runtime_seconds, + output_schema_json, + input_headers_json, + input_csv_path, + output_csv_path, + created_at, + updated_at, + started_at, + completed_at, + last_error +FROM agent_jobs +WHERE id = ? + "#, + ) + .bind(job_id) + .fetch_optional(self.pool.as_ref()) + .await?; + row.map(AgentJob::try_from).transpose() + } + + pub async fn list_agent_job_items( + &self, + job_id: &str, + status: Option, + limit: Option, + ) -> anyhow::Result> { + let mut builder = QueryBuilder::::new( + r#" +SELECT + job_id, + item_id, + row_index, + source_id, + row_json, + status, + assigned_thread_id, + attempt_count, + result_json, + last_error, + created_at, + updated_at, + completed_at, + reported_at +FROM agent_job_items +WHERE job_id = + "#, + ); + builder.push_bind(job_id); + if let Some(status) = status { + builder.push(" AND status = "); + builder.push_bind(status.as_str()); + } + builder.push(" ORDER BY row_index ASC"); + if let Some(limit) = limit { + builder.push(" LIMIT "); + builder.push_bind(limit as i64); + } + let rows: Vec = builder + .build_query_as::() + .fetch_all(self.pool.as_ref()) + .await?; + rows.into_iter().map(AgentJobItem::try_from).collect() + } + + pub async fn get_agent_job_item( + &self, + job_id: &str, + item_id: &str, + ) -> anyhow::Result> { + let row: Option = sqlx::query_as::<_, AgentJobItemRow>( + r#" +SELECT + job_id, + item_id, + row_index, + source_id, + row_json, + status, + assigned_thread_id, + attempt_count, + result_json, + last_error, + created_at, + updated_at, + completed_at, + reported_at +FROM agent_job_items +WHERE job_id = ? AND item_id = ? + "#, + ) + .bind(job_id) + .bind(item_id) + .fetch_optional(self.pool.as_ref()) + .await?; + row.map(AgentJobItem::try_from).transpose() + } + + pub async fn mark_agent_job_running(&self, job_id: &str) -> anyhow::Result<()> { + let now = Utc::now().timestamp(); + sqlx::query( + r#" +UPDATE agent_jobs +SET + status = ?, + updated_at = ?, + started_at = COALESCE(started_at, ?), + completed_at = NULL, + last_error = NULL +WHERE id = ? + "#, + ) + .bind(AgentJobStatus::Running.as_str()) + .bind(now) + .bind(now) + .bind(job_id) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + pub async fn mark_agent_job_completed(&self, job_id: &str) -> anyhow::Result<()> { + let now = Utc::now().timestamp(); + sqlx::query( + r#" +UPDATE agent_jobs +SET status = ?, updated_at = ?, completed_at = ?, last_error = NULL +WHERE id = ? + "#, + ) + .bind(AgentJobStatus::Completed.as_str()) + .bind(now) + .bind(now) + .bind(job_id) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + pub async fn mark_agent_job_failed( + &self, + job_id: &str, + error_message: &str, + ) -> anyhow::Result<()> { + let now = Utc::now().timestamp(); + sqlx::query( + r#" +UPDATE agent_jobs +SET status = ?, updated_at = ?, completed_at = ?, last_error = ? +WHERE id = ? + "#, + ) + .bind(AgentJobStatus::Failed.as_str()) + .bind(now) + .bind(now) + .bind(error_message) + .bind(job_id) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + pub async fn mark_agent_job_cancelled( + &self, + job_id: &str, + reason: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_jobs +SET status = ?, updated_at = ?, completed_at = ?, last_error = ? +WHERE id = ? AND status IN (?, ?) + "#, + ) + .bind(AgentJobStatus::Cancelled.as_str()) + .bind(now) + .bind(now) + .bind(reason) + .bind(job_id) + .bind(AgentJobStatus::Pending.as_str()) + .bind(AgentJobStatus::Running.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn is_agent_job_cancelled(&self, job_id: &str) -> anyhow::Result { + let row = sqlx::query( + r#" +SELECT status +FROM agent_jobs +WHERE id = ? + "#, + ) + .bind(job_id) + .fetch_optional(self.pool.as_ref()) + .await?; + let Some(row) = row else { + return Ok(false); + }; + let status: String = row.try_get("status")?; + Ok(AgentJobStatus::parse(status.as_str())? == AgentJobStatus::Cancelled) + } + + pub async fn mark_agent_job_item_running( + &self, + job_id: &str, + item_id: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + status = ?, + assigned_thread_id = NULL, + attempt_count = attempt_count + 1, + updated_at = ?, + last_error = NULL +WHERE job_id = ? AND item_id = ? AND status = ? + "#, + ) + .bind(AgentJobItemStatus::Running.as_str()) + .bind(now) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Pending.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn mark_agent_job_item_running_with_thread( + &self, + job_id: &str, + item_id: &str, + thread_id: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + status = ?, + assigned_thread_id = ?, + attempt_count = attempt_count + 1, + updated_at = ?, + last_error = NULL +WHERE job_id = ? AND item_id = ? AND status = ? + "#, + ) + .bind(AgentJobItemStatus::Running.as_str()) + .bind(thread_id) + .bind(now) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Pending.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn mark_agent_job_item_pending( + &self, + job_id: &str, + item_id: &str, + error_message: Option<&str>, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + status = ?, + assigned_thread_id = NULL, + updated_at = ?, + last_error = ? +WHERE job_id = ? AND item_id = ? AND status = ? + "#, + ) + .bind(AgentJobItemStatus::Pending.as_str()) + .bind(now) + .bind(error_message) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Running.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn set_agent_job_item_thread( + &self, + job_id: &str, + item_id: &str, + thread_id: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET assigned_thread_id = ?, updated_at = ? +WHERE job_id = ? AND item_id = ? AND status = ? + "#, + ) + .bind(thread_id) + .bind(now) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Running.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn report_agent_job_item_result( + &self, + job_id: &str, + item_id: &str, + reporting_thread_id: &str, + result_json: &Value, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let serialized = serde_json::to_string(result_json)?; + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + result_json = ?, + reported_at = ?, + updated_at = ?, + last_error = NULL +WHERE + job_id = ? + AND item_id = ? + AND status = ? + AND assigned_thread_id = ? + "#, + ) + .bind(serialized) + .bind(now) + .bind(now) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Running.as_str()) + .bind(reporting_thread_id) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn mark_agent_job_item_completed( + &self, + job_id: &str, + item_id: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + status = ?, + completed_at = ?, + updated_at = ?, + assigned_thread_id = NULL +WHERE + job_id = ? + AND item_id = ? + AND status = ? + AND result_json IS NOT NULL + "#, + ) + .bind(AgentJobItemStatus::Completed.as_str()) + .bind(now) + .bind(now) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Running.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn mark_agent_job_item_failed( + &self, + job_id: &str, + item_id: &str, + error_message: &str, + ) -> anyhow::Result { + let now = Utc::now().timestamp(); + let result = sqlx::query( + r#" +UPDATE agent_job_items +SET + status = ?, + completed_at = ?, + updated_at = ?, + last_error = ?, + assigned_thread_id = NULL +WHERE + job_id = ? + AND item_id = ? + AND status = ? + "#, + ) + .bind(AgentJobItemStatus::Failed.as_str()) + .bind(now) + .bind(now) + .bind(error_message) + .bind(job_id) + .bind(item_id) + .bind(AgentJobItemStatus::Running.as_str()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn get_agent_job_progress(&self, job_id: &str) -> anyhow::Result { + let row = sqlx::query( + r#" +SELECT + COUNT(*) AS total_items, + SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS pending_items, + SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS running_items, + SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS completed_items, + SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) AS failed_items +FROM agent_job_items +WHERE job_id = ? + "#, + ) + .bind(AgentJobItemStatus::Pending.as_str()) + .bind(AgentJobItemStatus::Running.as_str()) + .bind(AgentJobItemStatus::Completed.as_str()) + .bind(AgentJobItemStatus::Failed.as_str()) + .bind(job_id) + .fetch_one(self.pool.as_ref()) + .await?; + + let total_items: i64 = row.try_get("total_items")?; + let pending_items: Option = row.try_get("pending_items")?; + let running_items: Option = row.try_get("running_items")?; + let completed_items: Option = row.try_get("completed_items")?; + let failed_items: Option = row.try_get("failed_items")?; + Ok(AgentJobProgress { + total_items: usize::try_from(total_items).unwrap_or_default(), + pending_items: usize::try_from(pending_items.unwrap_or_default()).unwrap_or_default(), + running_items: usize::try_from(running_items.unwrap_or_default()).unwrap_or_default(), + completed_items: usize::try_from(completed_items.unwrap_or_default()) + .unwrap_or_default(), + failed_items: usize::try_from(failed_items.unwrap_or_default()).unwrap_or_default(), + }) + } +} diff --git a/codex-rs/state/src/runtime/backfill.rs b/codex-rs/state/src/runtime/backfill.rs new file mode 100644 index 0000000000..93e1569872 --- /dev/null +++ b/codex-rs/state/src/runtime/backfill.rs @@ -0,0 +1,311 @@ +use super::*; + +impl StateRuntime { + pub async fn get_backfill_state(&self) -> anyhow::Result { + self.ensure_backfill_state_row().await?; + let row = sqlx::query( + r#" +SELECT status, last_watermark, last_success_at +FROM backfill_state +WHERE id = 1 + "#, + ) + .fetch_one(self.pool.as_ref()) + .await?; + crate::BackfillState::try_from_row(&row) + } + + /// Attempt to claim ownership of rollout metadata backfill. + /// + /// Returns `true` when this runtime claimed the backfill worker slot. + /// Returns `false` if backfill is already complete or currently owned by a + /// non-expired worker. + pub async fn try_claim_backfill(&self, lease_seconds: i64) -> anyhow::Result { + self.ensure_backfill_state_row().await?; + let now = Utc::now().timestamp(); + let lease_cutoff = now.saturating_sub(lease_seconds.max(0)); + let result = sqlx::query( + r#" +UPDATE backfill_state +SET status = ?, updated_at = ? +WHERE id = 1 + AND status != ? + AND (status != ? OR updated_at <= ?) + "#, + ) + .bind(crate::BackfillStatus::Running.as_str()) + .bind(now) + .bind(crate::BackfillStatus::Complete.as_str()) + .bind(crate::BackfillStatus::Running.as_str()) + .bind(lease_cutoff) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected() == 1) + } + + /// Mark rollout metadata backfill as running. + pub async fn mark_backfill_running(&self) -> anyhow::Result<()> { + self.ensure_backfill_state_row().await?; + sqlx::query( + r#" +UPDATE backfill_state +SET status = ?, updated_at = ? +WHERE id = 1 + "#, + ) + .bind(crate::BackfillStatus::Running.as_str()) + .bind(Utc::now().timestamp()) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + /// Persist rollout metadata backfill progress. + pub async fn checkpoint_backfill(&self, watermark: &str) -> anyhow::Result<()> { + self.ensure_backfill_state_row().await?; + sqlx::query( + r#" +UPDATE backfill_state +SET status = ?, last_watermark = ?, updated_at = ? +WHERE id = 1 + "#, + ) + .bind(crate::BackfillStatus::Running.as_str()) + .bind(watermark) + .bind(Utc::now().timestamp()) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + /// Mark rollout metadata backfill as complete. + pub async fn mark_backfill_complete(&self, last_watermark: Option<&str>) -> anyhow::Result<()> { + self.ensure_backfill_state_row().await?; + let now = Utc::now().timestamp(); + sqlx::query( + r#" +UPDATE backfill_state +SET + status = ?, + last_watermark = COALESCE(?, last_watermark), + last_success_at = ?, + updated_at = ? +WHERE id = 1 + "#, + ) + .bind(crate::BackfillStatus::Complete.as_str()) + .bind(last_watermark) + .bind(now) + .bind(now) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + async fn ensure_backfill_state_row(&self) -> anyhow::Result<()> { + sqlx::query( + r#" +INSERT INTO backfill_state (id, status, last_watermark, last_success_at, updated_at) +VALUES (?, ?, NULL, NULL, ?) +ON CONFLICT(id) DO NOTHING + "#, + ) + .bind(1_i64) + .bind(crate::BackfillStatus::Pending.as_str()) + .bind(Utc::now().timestamp()) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::StateRuntime; + use super::state_db_filename; + use super::test_support::unique_temp_dir; + use crate::STATE_DB_FILENAME; + use crate::STATE_DB_VERSION; + use chrono::Utc; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn init_removes_legacy_state_db_files() { + let codex_home = unique_temp_dir(); + tokio::fs::create_dir_all(&codex_home) + .await + .expect("create codex_home"); + + let current_name = state_db_filename(); + let previous_version = STATE_DB_VERSION.saturating_sub(1); + let unversioned_name = format!("{STATE_DB_FILENAME}.sqlite"); + for suffix in ["", "-wal", "-shm", "-journal"] { + let path = codex_home.join(format!("{unversioned_name}{suffix}")); + tokio::fs::write(path, b"legacy") + .await + .expect("write legacy"); + let old_version_path = codex_home.join(format!( + "{STATE_DB_FILENAME}_{previous_version}.sqlite{suffix}" + )); + tokio::fs::write(old_version_path, b"old_version") + .await + .expect("write old version"); + } + let unrelated_path = codex_home.join("state.sqlite_backup"); + tokio::fs::write(&unrelated_path, b"keep") + .await + .expect("write unrelated"); + let numeric_path = codex_home.join("123"); + tokio::fs::write(&numeric_path, b"keep") + .await + .expect("write numeric"); + + let _runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + for suffix in ["", "-wal", "-shm", "-journal"] { + let legacy_path = codex_home.join(format!("{unversioned_name}{suffix}")); + assert_eq!( + tokio::fs::try_exists(&legacy_path) + .await + .expect("check legacy path"), + false + ); + let old_version_path = codex_home.join(format!( + "{STATE_DB_FILENAME}_{previous_version}.sqlite{suffix}" + )); + assert_eq!( + tokio::fs::try_exists(&old_version_path) + .await + .expect("check old version path"), + false + ); + } + assert_eq!( + tokio::fs::try_exists(codex_home.join(current_name)) + .await + .expect("check new db path"), + true + ); + assert_eq!( + tokio::fs::try_exists(&unrelated_path) + .await + .expect("check unrelated path"), + true + ); + assert_eq!( + tokio::fs::try_exists(&numeric_path) + .await + .expect("check numeric path"), + true + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn backfill_state_persists_progress_and_completion() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let initial = runtime + .get_backfill_state() + .await + .expect("get initial backfill state"); + assert_eq!(initial.status, crate::BackfillStatus::Pending); + assert_eq!(initial.last_watermark, None); + assert_eq!(initial.last_success_at, None); + + runtime + .mark_backfill_running() + .await + .expect("mark backfill running"); + runtime + .checkpoint_backfill("sessions/2026/01/27/rollout-a.jsonl") + .await + .expect("checkpoint backfill"); + + let running = runtime + .get_backfill_state() + .await + .expect("get running backfill state"); + assert_eq!(running.status, crate::BackfillStatus::Running); + assert_eq!( + running.last_watermark, + Some("sessions/2026/01/27/rollout-a.jsonl".to_string()) + ); + assert_eq!(running.last_success_at, None); + + runtime + .mark_backfill_complete(Some("sessions/2026/01/28/rollout-b.jsonl")) + .await + .expect("mark backfill complete"); + let completed = runtime + .get_backfill_state() + .await + .expect("get completed backfill state"); + assert_eq!(completed.status, crate::BackfillStatus::Complete); + assert_eq!( + completed.last_watermark, + Some("sessions/2026/01/28/rollout-b.jsonl".to_string()) + ); + assert!(completed.last_success_at.is_some()); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn backfill_claim_is_singleton_until_stale_and_blocked_when_complete() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let claimed = runtime + .try_claim_backfill(3600) + .await + .expect("initial backfill claim"); + assert_eq!(claimed, true); + + let duplicate_claim = runtime + .try_claim_backfill(3600) + .await + .expect("duplicate backfill claim"); + assert_eq!(duplicate_claim, false); + + let stale_updated_at = Utc::now().timestamp().saturating_sub(10_000); + sqlx::query( + r#" +UPDATE backfill_state +SET status = ?, updated_at = ? +WHERE id = 1 + "#, + ) + .bind(crate::BackfillStatus::Running.as_str()) + .bind(stale_updated_at) + .execute(runtime.pool.as_ref()) + .await + .expect("force stale backfill lease"); + + let stale_claim = runtime + .try_claim_backfill(10) + .await + .expect("stale backfill claim"); + assert_eq!(stale_claim, true); + + runtime + .mark_backfill_complete(None) + .await + .expect("mark complete"); + let claim_after_complete = runtime + .try_claim_backfill(3600) + .await + .expect("claim after complete"); + assert_eq!(claim_after_complete, false); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } +} diff --git a/codex-rs/state/src/runtime/logs.rs b/codex-rs/state/src/runtime/logs.rs new file mode 100644 index 0000000000..762f68f1a9 --- /dev/null +++ b/codex-rs/state/src/runtime/logs.rs @@ -0,0 +1,715 @@ +use super::*; + +impl StateRuntime { + pub async fn insert_log(&self, entry: &LogEntry) -> anyhow::Result<()> { + self.insert_logs(std::slice::from_ref(entry)).await + } + + /// Insert a batch of log entries into the logs table. + pub async fn insert_logs(&self, entries: &[LogEntry]) -> anyhow::Result<()> { + if entries.is_empty() { + return Ok(()); + } + + let mut tx = self.pool.begin().await?; + let mut builder = QueryBuilder::::new( + "INSERT INTO logs (ts, ts_nanos, level, target, message, thread_id, process_uuid, module_path, file, line, estimated_bytes) ", + ); + builder.push_values(entries, |mut row, entry| { + let estimated_bytes = entry.message.as_ref().map_or(0, String::len) as i64 + + entry.level.len() as i64 + + entry.target.len() as i64 + + entry.module_path.as_ref().map_or(0, String::len) as i64 + + entry.file.as_ref().map_or(0, String::len) as i64; + row.push_bind(entry.ts) + .push_bind(entry.ts_nanos) + .push_bind(&entry.level) + .push_bind(&entry.target) + .push_bind(&entry.message) + .push_bind(&entry.thread_id) + .push_bind(&entry.process_uuid) + .push_bind(&entry.module_path) + .push_bind(&entry.file) + .push_bind(entry.line) + .push_bind(estimated_bytes); + }); + builder.build().execute(&mut *tx).await?; + self.prune_logs_after_insert(entries, &mut tx).await?; + tx.commit().await?; + Ok(()) + } + + /// Enforce per-partition log size caps after a successful batch insert. + /// + /// We maintain two independent budgets: + /// - Thread logs: rows with `thread_id IS NOT NULL`, capped per `thread_id`. + /// - Threadless process logs: rows with `thread_id IS NULL` ("threadless"), + /// capped per `process_uuid` (including `process_uuid IS NULL` as its own + /// threadless partition). + /// + /// "Threadless" means the log row is not associated with any conversation + /// thread, so retention is keyed by process identity instead. + /// + /// This runs inside the same transaction as the insert so callers never + /// observe "inserted but not yet pruned" rows. + async fn prune_logs_after_insert( + &self, + entries: &[LogEntry], + tx: &mut SqliteConnection, + ) -> anyhow::Result<()> { + let thread_ids: BTreeSet<&str> = entries + .iter() + .filter_map(|entry| entry.thread_id.as_deref()) + .collect(); + if !thread_ids.is_empty() { + // Cheap precheck: only run the heavier window-function prune for + // threads that are currently above the cap. + let mut over_limit_threads_query = + QueryBuilder::::new("SELECT thread_id FROM logs WHERE thread_id IN ("); + { + let mut separated = over_limit_threads_query.separated(", "); + for thread_id in &thread_ids { + separated.push_bind(*thread_id); + } + } + over_limit_threads_query.push(") GROUP BY thread_id HAVING SUM("); + over_limit_threads_query.push("estimated_bytes"); + over_limit_threads_query.push(") > "); + over_limit_threads_query.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + let over_limit_thread_ids: Vec = over_limit_threads_query + .build() + .fetch_all(&mut *tx) + .await? + .into_iter() + .map(|row| row.try_get("thread_id")) + .collect::>()?; + if !over_limit_thread_ids.is_empty() { + // Enforce a strict per-thread cap by deleting every row whose + // newest-first cumulative bytes exceed the partition budget. + let mut prune_threads = QueryBuilder::::new( + r#" +DELETE FROM logs +WHERE id IN ( + SELECT id + FROM ( + SELECT + id, + SUM( +"#, + ); + prune_threads.push("estimated_bytes"); + prune_threads.push( + r#" + ) OVER ( + PARTITION BY thread_id + ORDER BY ts DESC, ts_nanos DESC, id DESC + ) AS cumulative_bytes + FROM logs + WHERE thread_id IN ( +"#, + ); + { + let mut separated = prune_threads.separated(", "); + for thread_id in &over_limit_thread_ids { + separated.push_bind(thread_id); + } + } + prune_threads.push( + r#" + ) + ) + WHERE cumulative_bytes > +"#, + ); + prune_threads.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + prune_threads.push("\n)"); + prune_threads.build().execute(&mut *tx).await?; + } + } + + let threadless_process_uuids: BTreeSet<&str> = entries + .iter() + .filter(|entry| entry.thread_id.is_none()) + .filter_map(|entry| entry.process_uuid.as_deref()) + .collect(); + let has_threadless_null_process_uuid = entries + .iter() + .any(|entry| entry.thread_id.is_none() && entry.process_uuid.is_none()); + if !threadless_process_uuids.is_empty() { + // Threadless logs are budgeted separately per process UUID. + let mut over_limit_processes_query = QueryBuilder::::new( + "SELECT process_uuid FROM logs WHERE thread_id IS NULL AND process_uuid IN (", + ); + { + let mut separated = over_limit_processes_query.separated(", "); + for process_uuid in &threadless_process_uuids { + separated.push_bind(*process_uuid); + } + } + over_limit_processes_query.push(") GROUP BY process_uuid HAVING SUM("); + over_limit_processes_query.push("estimated_bytes"); + over_limit_processes_query.push(") > "); + over_limit_processes_query.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + let over_limit_process_uuids: Vec = over_limit_processes_query + .build() + .fetch_all(&mut *tx) + .await? + .into_iter() + .map(|row| row.try_get("process_uuid")) + .collect::>()?; + if !over_limit_process_uuids.is_empty() { + // Same strict cap policy as thread pruning, but only for + // threadless rows in the affected process UUIDs. + let mut prune_threadless_process_logs = QueryBuilder::::new( + r#" +DELETE FROM logs +WHERE id IN ( + SELECT id + FROM ( + SELECT + id, + SUM( +"#, + ); + prune_threadless_process_logs.push("estimated_bytes"); + prune_threadless_process_logs.push( + r#" + ) OVER ( + PARTITION BY process_uuid + ORDER BY ts DESC, ts_nanos DESC, id DESC + ) AS cumulative_bytes + FROM logs + WHERE thread_id IS NULL + AND process_uuid IN ( +"#, + ); + { + let mut separated = prune_threadless_process_logs.separated(", "); + for process_uuid in &over_limit_process_uuids { + separated.push_bind(process_uuid); + } + } + prune_threadless_process_logs.push( + r#" + ) + ) + WHERE cumulative_bytes > +"#, + ); + prune_threadless_process_logs.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + prune_threadless_process_logs.push("\n)"); + prune_threadless_process_logs + .build() + .execute(&mut *tx) + .await?; + } + } + if has_threadless_null_process_uuid { + // Rows without a process UUID still need a cap; treat NULL as its + // own threadless partition. + let mut null_process_usage_query = QueryBuilder::::new("SELECT SUM("); + null_process_usage_query.push("estimated_bytes"); + null_process_usage_query.push( + ") AS total_bytes FROM logs WHERE thread_id IS NULL AND process_uuid IS NULL", + ); + let total_null_process_bytes: Option = null_process_usage_query + .build() + .fetch_one(&mut *tx) + .await? + .try_get("total_bytes")?; + + if total_null_process_bytes.unwrap_or(0) > LOG_PARTITION_SIZE_LIMIT_BYTES { + let mut prune_threadless_null_process_logs = QueryBuilder::::new( + r#" +DELETE FROM logs +WHERE id IN ( + SELECT id + FROM ( + SELECT + id, + SUM( +"#, + ); + prune_threadless_null_process_logs.push("estimated_bytes"); + prune_threadless_null_process_logs.push( + r#" + ) OVER ( + PARTITION BY process_uuid + ORDER BY ts DESC, ts_nanos DESC, id DESC + ) AS cumulative_bytes + FROM logs + WHERE thread_id IS NULL + AND process_uuid IS NULL + ) + WHERE cumulative_bytes > +"#, + ); + prune_threadless_null_process_logs.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + prune_threadless_null_process_logs.push("\n)"); + prune_threadless_null_process_logs + .build() + .execute(&mut *tx) + .await?; + } + } + Ok(()) + } + + pub(crate) async fn delete_logs_before(&self, cutoff_ts: i64) -> anyhow::Result { + let result = sqlx::query("DELETE FROM logs WHERE ts < ?") + .bind(cutoff_ts) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected()) + } + + /// Query logs with optional filters. + pub async fn query_logs(&self, query: &LogQuery) -> anyhow::Result> { + let mut builder = QueryBuilder::::new( + "SELECT id, ts, ts_nanos, level, target, message, thread_id, process_uuid, file, line FROM logs WHERE 1 = 1", + ); + push_log_filters(&mut builder, query); + if query.descending { + builder.push(" ORDER BY id DESC"); + } else { + builder.push(" ORDER BY id ASC"); + } + if let Some(limit) = query.limit { + builder.push(" LIMIT ").push_bind(limit as i64); + } + + let rows = builder + .build_query_as::() + .fetch_all(self.pool.as_ref()) + .await?; + Ok(rows) + } + + /// Return the max log id matching optional filters. + pub async fn max_log_id(&self, query: &LogQuery) -> anyhow::Result { + let mut builder = + QueryBuilder::::new("SELECT MAX(id) AS max_id FROM logs WHERE 1 = 1"); + push_log_filters(&mut builder, query); + let row = builder.build().fetch_one(self.pool.as_ref()).await?; + let max_id: Option = row.try_get("max_id")?; + Ok(max_id.unwrap_or(0)) + } +} + +fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQuery) { + if let Some(level_upper) = query.level_upper.as_ref() { + builder + .push(" AND UPPER(level) = ") + .push_bind(level_upper.as_str()); + } + if let Some(from_ts) = query.from_ts { + builder.push(" AND ts >= ").push_bind(from_ts); + } + if let Some(to_ts) = query.to_ts { + builder.push(" AND ts <= ").push_bind(to_ts); + } + push_like_filters(builder, "module_path", &query.module_like); + push_like_filters(builder, "file", &query.file_like); + let has_thread_filter = !query.thread_ids.is_empty() || query.include_threadless; + if has_thread_filter { + builder.push(" AND ("); + let mut needs_or = false; + for thread_id in &query.thread_ids { + if needs_or { + builder.push(" OR "); + } + builder.push("thread_id = ").push_bind(thread_id.as_str()); + needs_or = true; + } + if query.include_threadless { + if needs_or { + builder.push(" OR "); + } + builder.push("thread_id IS NULL"); + } + builder.push(")"); + } + if let Some(after_id) = query.after_id { + builder.push(" AND id > ").push_bind(after_id); + } + if let Some(search) = query.search.as_ref() { + builder.push(" AND INSTR(message, "); + builder.push_bind(search.as_str()); + builder.push(") > 0"); + } +} + +fn push_like_filters<'a>( + builder: &mut QueryBuilder<'a, Sqlite>, + column: &str, + filters: &'a [String], +) { + if filters.is_empty() { + return; + } + builder.push(" AND ("); + for (idx, filter) in filters.iter().enumerate() { + if idx > 0 { + builder.push(" OR "); + } + builder + .push(column) + .push(" LIKE '%' || ") + .push_bind(filter.as_str()) + .push(" || '%'"); + } + builder.push(")"); +} + +#[cfg(test)] +mod tests { + use super::StateRuntime; + use super::test_support::unique_temp_dir; + use crate::LogEntry; + use crate::LogQuery; + use pretty_assertions::assert_eq; + #[tokio::test] + async fn query_logs_with_search_matches_substring() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .insert_logs(&[ + LogEntry { + ts: 1_700_000_001, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some("alpha".to_string()), + thread_id: Some("thread-1".to_string()), + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(42), + module_path: None, + }, + LogEntry { + ts: 1_700_000_002, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some("alphabet".to_string()), + thread_id: Some("thread-1".to_string()), + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(43), + module_path: None, + }, + ]) + .await + .expect("insert test logs"); + + let rows = runtime + .query_logs(&LogQuery { + search: Some("alphab".to_string()), + ..Default::default() + }) + .await + .expect("query matching logs"); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].message.as_deref(), Some("alphabet")); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_old_rows_when_thread_exceeds_size_limit() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let six_mebibytes = "a".repeat(6 * 1024 * 1024); + runtime + .insert_logs(&[ + LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes.clone()), + thread_id: Some("thread-1".to_string()), + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }, + LogEntry { + ts: 2, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes.clone()), + thread_id: Some("thread-1".to_string()), + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(2), + module_path: Some("mod".to_string()), + }, + ]) + .await + .expect("insert test logs"); + + let rows = runtime + .query_logs(&LogQuery { + thread_ids: vec!["thread-1".to_string()], + ..Default::default() + }) + .await + .expect("query thread logs"); + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].ts, 2); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let eleven_mebibytes = "d".repeat(11 * 1024 * 1024); + runtime + .insert_logs(&[LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(eleven_mebibytes), + thread_id: Some("thread-oversized".to_string()), + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }]) + .await + .expect("insert test log"); + + let rows = runtime + .query_logs(&LogQuery { + thread_ids: vec!["thread-oversized".to_string()], + ..Default::default() + }) + .await + .expect("query thread logs"); + + assert!(rows.is_empty()); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_threadless_rows_per_process_uuid_only() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let six_mebibytes = "b".repeat(6 * 1024 * 1024); + runtime + .insert_logs(&[ + LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes.clone()), + thread_id: None, + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }, + LogEntry { + ts: 2, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes.clone()), + thread_id: None, + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(2), + module_path: Some("mod".to_string()), + }, + LogEntry { + ts: 3, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes), + thread_id: Some("thread-1".to_string()), + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(3), + module_path: Some("mod".to_string()), + }, + ]) + .await + .expect("insert test logs"); + + let rows = runtime + .query_logs(&LogQuery { + thread_ids: vec!["thread-1".to_string()], + include_threadless: true, + ..Default::default() + }) + .await + .expect("query thread and threadless logs"); + + let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); + timestamps.sort_unstable(); + assert_eq!(timestamps, vec![2, 3]); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let eleven_mebibytes = "e".repeat(11 * 1024 * 1024); + runtime + .insert_logs(&[LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(eleven_mebibytes), + thread_id: None, + process_uuid: Some("proc-oversized".to_string()), + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }]) + .await + .expect("insert test log"); + + let rows = runtime + .query_logs(&LogQuery { + include_threadless: true, + ..Default::default() + }) + .await + .expect("query threadless logs"); + + assert!(rows.is_empty()); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_threadless_rows_with_null_process_uuid() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let six_mebibytes = "c".repeat(6 * 1024 * 1024); + runtime + .insert_logs(&[ + LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes.clone()), + thread_id: None, + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }, + LogEntry { + ts: 2, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(six_mebibytes), + thread_id: None, + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(2), + module_path: Some("mod".to_string()), + }, + LogEntry { + ts: 3, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some("small".to_string()), + thread_id: None, + process_uuid: Some("proc-1".to_string()), + file: Some("main.rs".to_string()), + line: Some(3), + module_path: Some("mod".to_string()), + }, + ]) + .await + .expect("insert test logs"); + + let rows = runtime + .query_logs(&LogQuery { + include_threadless: true, + ..Default::default() + }) + .await + .expect("query threadless logs"); + + let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); + timestamps.sort_unstable(); + assert_eq!(timestamps, vec![2, 3]); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let eleven_mebibytes = "f".repeat(11 * 1024 * 1024); + runtime + .insert_logs(&[LogEntry { + ts: 1, + ts_nanos: 0, + level: "INFO".to_string(), + target: "cli".to_string(), + message: Some(eleven_mebibytes), + thread_id: None, + process_uuid: None, + file: Some("main.rs".to_string()), + line: Some(1), + module_path: Some("mod".to_string()), + }]) + .await + .expect("insert test log"); + + let rows = runtime + .query_logs(&LogQuery { + include_threadless: true, + ..Default::default() + }) + .await + .expect("query threadless logs"); + + assert!(rows.is_empty()); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } +} diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index bad5679eac..908919d0aa 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -1,4 +1,7 @@ +use super::threads::push_thread_filters; +use super::threads::push_thread_order_and_limit; use super::*; +use crate::model::Phase2InputSelection; use crate::model::Phase2JobClaimOutcome; use crate::model::Stage1JobClaim; use crate::model::Stage1JobClaimOutcome; @@ -6,10 +9,13 @@ use crate::model::Stage1Output; use crate::model::Stage1OutputRow; use crate::model::Stage1StartupClaimParams; use crate::model::ThreadRow; +use crate::model::stage1_output_ref_from_parts; use chrono::Duration; use sqlx::Executor; use sqlx::QueryBuilder; use sqlx::Sqlite; +use std::collections::HashSet; +use uuid::Uuid; const JOB_KIND_MEMORY_STAGE1: &str = "memory_stage1"; const JOB_KIND_MEMORY_CONSOLIDATE_GLOBAL: &str = "memory_consolidate_global"; @@ -49,11 +55,49 @@ WHERE kind = ? OR kind = ? Ok(()) } + /// Record usage for cited stage-1 outputs. + /// + /// Each thread id increments `usage_count` by one and sets `last_usage` to + /// the current Unix timestamp. Missing rows are ignored. + pub async fn record_stage1_output_usage( + &self, + thread_ids: &[ThreadId], + ) -> anyhow::Result { + if thread_ids.is_empty() { + return Ok(0); + } + + let now = Utc::now().timestamp(); + let mut tx = self.pool.begin().await?; + let mut updated_rows = 0; + + for thread_id in thread_ids { + updated_rows += sqlx::query( + r#" +UPDATE stage1_outputs +SET + usage_count = COALESCE(usage_count, 0) + 1, + last_usage = ? +WHERE thread_id = ? + "#, + ) + .bind(now) + .bind(thread_id.to_string()) + .execute(&mut *tx) + .await? + .rows_affected() as usize; + } + + tx.commit().await?; + Ok(updated_rows) + } + /// Selects and claims stage-1 startup jobs for stale threads. /// /// Query behavior: /// - starts from `threads` filtered to active threads and allowed sources /// (`push_thread_filters`) + /// - excludes threads with `memory_mode != 'enabled'` /// - excludes the current thread id /// - keeps only threads in the age window: /// `updated_at >= now - max_age_days` and `updated_at <= now - min_rollout_idle_hours` @@ -129,7 +173,9 @@ LEFT JOIN jobs None, None, SortKey::UpdatedAt, + None, ); + builder.push(" AND threads.memory_mode = 'enabled'"); builder .push(" AND id != ") .push_bind(current_thread_id.as_str()); @@ -180,7 +226,7 @@ LEFT JOIN jobs /// /// Query behavior: /// - filters out rows where both `raw_memory` and `rollout_summary` are blank - /// - joins `threads` to include thread `cwd` + /// - joins `threads` to include thread `cwd`, `rollout_path`, and `git_branch` /// - orders by `source_updated_at DESC, thread_id DESC` /// - applies `LIMIT n` pub async fn list_stage1_outputs_for_global( @@ -195,12 +241,14 @@ LEFT JOIN jobs r#" SELECT so.thread_id, + COALESCE(t.rollout_path, '') AS rollout_path, so.source_updated_at, so.raw_memory, so.rollout_summary, so.rollout_slug, - so.generated_at - , COALESCE(t.cwd, '') AS cwd + so.generated_at, + COALESCE(t.cwd, '') AS cwd, + t.git_branch AS git_branch FROM stage1_outputs AS so LEFT JOIN threads AS t ON t.id = so.thread_id @@ -218,6 +266,136 @@ LIMIT ? .collect::, _>>() } + /// Returns the current phase-2 input set along with its diff against the + /// last successful phase-2 selection. + /// + /// Query behavior: + /// - current selection keeps only non-empty stage-1 outputs whose + /// `last_usage` is within `max_unused_days`, or whose + /// `source_updated_at` is within that window when the memory has never + /// been used + /// - eligible rows are ordered by `usage_count DESC`, + /// `COALESCE(last_usage, source_updated_at) DESC`, `source_updated_at DESC`, + /// `thread_id DESC` + /// - previously selected rows are identified by `selected_for_phase2 = 1` + /// - `previous_selected` contains the current persisted rows that belonged + /// to the last successful phase-2 baseline + /// - `retained_thread_ids` records which current rows still match the exact + /// snapshot selected in the last successful phase-2 run + /// - removed rows are previously selected rows that are still present in + /// `stage1_outputs` but fall outside the current top-`n` selection + pub async fn get_phase2_input_selection( + &self, + n: usize, + max_unused_days: i64, + ) -> anyhow::Result { + if n == 0 { + return Ok(Phase2InputSelection::default()); + } + let cutoff = (Utc::now() - Duration::days(max_unused_days.max(0))).timestamp(); + + let current_rows = sqlx::query( + r#" +SELECT + so.thread_id, + COALESCE(t.rollout_path, '') AS rollout_path, + so.source_updated_at, + so.raw_memory, + so.rollout_summary, + so.rollout_slug, + so.generated_at, + COALESCE(t.cwd, '') AS cwd, + t.git_branch AS git_branch, + so.selected_for_phase2, + so.selected_for_phase2_source_updated_at +FROM stage1_outputs AS so +LEFT JOIN threads AS t + ON t.id = so.thread_id +WHERE (length(trim(so.raw_memory)) > 0 OR length(trim(so.rollout_summary)) > 0) + AND ( + (so.last_usage IS NOT NULL AND so.last_usage >= ?) + OR (so.last_usage IS NULL AND so.source_updated_at >= ?) + ) +ORDER BY + COALESCE(so.usage_count, 0) DESC, + COALESCE(so.last_usage, so.source_updated_at) DESC, + so.source_updated_at DESC, + so.thread_id DESC +LIMIT ? + "#, + ) + .bind(cutoff) + .bind(cutoff) + .bind(n as i64) + .fetch_all(self.pool.as_ref()) + .await?; + + let mut current_thread_ids = HashSet::with_capacity(current_rows.len()); + let mut selected = Vec::with_capacity(current_rows.len()); + let mut retained_thread_ids = Vec::new(); + for row in current_rows { + let thread_id = row.try_get::("thread_id")?; + current_thread_ids.insert(thread_id.clone()); + let source_updated_at = row.try_get::("source_updated_at")?; + if row.try_get::("selected_for_phase2")? != 0 + && row.try_get::, _>("selected_for_phase2_source_updated_at")? + == Some(source_updated_at) + { + retained_thread_ids.push(ThreadId::try_from(thread_id.clone())?); + } + selected.push(Stage1Output::try_from(Stage1OutputRow::try_from_row( + &row, + )?)?); + } + + let previous_rows = sqlx::query( + r#" +SELECT + so.thread_id, + COALESCE(t.rollout_path, '') AS rollout_path, + so.source_updated_at, + so.raw_memory, + so.rollout_summary, + so.rollout_slug, + so.generated_at, + COALESCE(t.cwd, '') AS cwd, + t.git_branch AS git_branch +FROM stage1_outputs AS so +LEFT JOIN threads AS t + ON t.id = so.thread_id +WHERE so.selected_for_phase2 = 1 +ORDER BY so.source_updated_at DESC, so.thread_id DESC + "#, + ) + .fetch_all(self.pool.as_ref()) + .await?; + + let previous_selected = previous_rows + .iter() + .map(Stage1OutputRow::try_from_row) + .map(|row| row.and_then(Stage1Output::try_from)) + .collect::, _>>()?; + let mut removed = Vec::new(); + for row in previous_rows { + let thread_id = row.try_get::("thread_id")?; + if current_thread_ids.contains(thread_id.as_str()) { + continue; + } + removed.push(stage1_output_ref_from_parts( + thread_id, + row.try_get("source_updated_at")?, + row.try_get("rollout_slug")?, + )?); + } + + Ok(Phase2InputSelection { + selected, + previous_selected, + retained_thread_ids, + removed, + }) + } + /// Attempts to claim a stage-1 job for a thread at `source_updated_at`. /// /// Claim semantics: @@ -415,6 +593,9 @@ WHERE kind = ? AND job_key = ? /// - sets `status='done'` and `last_success_watermark = input_watermark` /// - upserts `stage1_outputs` for the thread, replacing existing output only /// when `source_updated_at` is newer or equal + /// - preserves any existing `selected_for_phase2` baseline until the next + /// successful phase-2 run rewrites the baseline selection, including the + /// snapshot timestamp chosen during that run /// - persists optional `rollout_slug` for rollout summary artifact naming /// - enqueues/advances the global phase-2 job watermark using /// `source_updated_at` @@ -767,12 +948,18 @@ WHERE kind = ? AND job_key = ? /// - sets `status='done'`, clears lease/errors /// - advances `last_success_watermark` to /// `max(existing_last_success_watermark, completed_watermark)` + /// - rewrites `selected_for_phase2` so only the exact selected stage-1 + /// snapshots remain marked as part of the latest successful phase-2 + /// selection, and persists each selected snapshot's + /// `source_updated_at` for future retained-vs-added diffing pub async fn mark_global_phase2_job_succeeded( &self, ownership_token: &str, completed_watermark: i64, + selected_outputs: &[Stage1Output], ) -> anyhow::Result { let now = Utc::now().timestamp(); + let mut tx = self.pool.begin().await?; let rows_affected = sqlx::query( r#" UPDATE jobs @@ -791,11 +978,46 @@ WHERE kind = ? AND job_key = ? .bind(JOB_KIND_MEMORY_CONSOLIDATE_GLOBAL) .bind(MEMORY_CONSOLIDATION_JOB_KEY) .bind(ownership_token) - .execute(self.pool.as_ref()) + .execute(&mut *tx) .await? .rows_affected(); - Ok(rows_affected > 0) + if rows_affected == 0 { + tx.commit().await?; + return Ok(false); + } + + sqlx::query( + r#" +UPDATE stage1_outputs +SET + selected_for_phase2 = 0, + selected_for_phase2_source_updated_at = NULL +WHERE selected_for_phase2 != 0 OR selected_for_phase2_source_updated_at IS NOT NULL + "#, + ) + .execute(&mut *tx) + .await?; + + for output in selected_outputs { + sqlx::query( + r#" +UPDATE stage1_outputs +SET + selected_for_phase2 = 1, + selected_for_phase2_source_updated_at = ? +WHERE thread_id = ? AND source_updated_at = ? + "#, + ) + .bind(output.source_updated_at.timestamp()) + .bind(output.thread_id.to_string()) + .bind(output.source_updated_at.timestamp()) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(true) } /// Marks the owned running global phase-2 job as failed and schedules retry. @@ -933,3 +1155,2586 @@ ON CONFLICT(kind, job_key) DO UPDATE SET Ok(()) } + +#[cfg(test)] +mod tests { + use super::StateRuntime; + use super::test_support::test_thread_metadata; + use super::test_support::unique_temp_dir; + use crate::model::Phase2JobClaimOutcome; + use crate::model::Stage1JobClaimOutcome; + use crate::model::Stage1StartupClaimParams; + use chrono::Duration; + use chrono::Utc; + use codex_protocol::ThreadId; + use pretty_assertions::assert_eq; + use sqlx::Row; + use std::sync::Arc; + use uuid::Uuid; + + #[tokio::test] + async fn stage1_claim_skips_when_up_to_date() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let metadata = test_thread_metadata(&codex_home, thread_id, codex_home.join("a")); + runtime + .upsert_thread(&metadata) + .await + .expect("upsert thread"); + + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + let claim = runtime + .try_claim_stage1_job(thread_id, owner_a, 100, 3600, 64) + .await + .expect("claim stage1 job"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected claim outcome: {other:?}"), + }; + + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + 100, + "raw", + "sum", + None, + ) + .await + .expect("mark stage1 succeeded"), + "stage1 success should finalize for current token" + ); + + let up_to_date = runtime + .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) + .await + .expect("claim stage1 up-to-date"); + assert_eq!(up_to_date, Stage1JobClaimOutcome::SkippedUpToDate); + + let needs_rerun = runtime + .try_claim_stage1_job(thread_id, owner_b, 101, 3600, 64) + .await + .expect("claim stage1 newer source"); + assert!( + matches!(needs_rerun, Stage1JobClaimOutcome::Claimed { .. }), + "newer source_updated_at should be claimable" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn stage1_running_stale_can_be_stolen_but_fresh_running_is_skipped() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let cwd = codex_home.join("workspace"); + runtime + .upsert_thread(&test_thread_metadata(&codex_home, thread_id, cwd)) + .await + .expect("upsert thread"); + + let claim_a = runtime + .try_claim_stage1_job(thread_id, owner_a, 100, 3600, 64) + .await + .expect("claim a"); + assert!(matches!(claim_a, Stage1JobClaimOutcome::Claimed { .. })); + + let claim_b_fresh = runtime + .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) + .await + .expect("claim b fresh"); + assert_eq!(claim_b_fresh, Stage1JobClaimOutcome::SkippedRunning); + + sqlx::query("UPDATE jobs SET lease_until = 0 WHERE kind = 'memory_stage1' AND job_key = ?") + .bind(thread_id.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("force stale lease"); + + let claim_b_stale = runtime + .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) + .await + .expect("claim b stale"); + assert!(matches!( + claim_b_stale, + Stage1JobClaimOutcome::Claimed { .. } + )); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn stage1_concurrent_claim_for_same_thread_is_conflict_safe() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let thread_id_a = thread_id; + let thread_id_b = thread_id; + let runtime_a = Arc::clone(&runtime); + let runtime_b = Arc::clone(&runtime); + let claim_with_retry = |runtime: Arc, + thread_id: ThreadId, + owner: ThreadId| async move { + for attempt in 0..5 { + match runtime + .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) + .await + { + Ok(outcome) => return outcome, + Err(err) if err.to_string().contains("database is locked") && attempt < 4 => { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + Err(err) => panic!("claim stage1 should not fail: {err}"), + } + } + panic!("claim stage1 should have returned within retry budget") + }; + + let (claim_a, claim_b) = tokio::join!( + claim_with_retry(runtime_a, thread_id_a, owner_a), + claim_with_retry(runtime_b, thread_id_b, owner_b), + ); + + let claim_outcomes = vec![claim_a, claim_b]; + let claimed_count = claim_outcomes + .iter() + .filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. })) + .count(); + assert_eq!(claimed_count, 1); + assert!( + claim_outcomes.iter().all(|outcome| { + matches!( + outcome, + Stage1JobClaimOutcome::Claimed { .. } | Stage1JobClaimOutcome::SkippedRunning + ) + }), + "unexpected claim outcomes: {claim_outcomes:?}" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn stage1_concurrent_claims_respect_running_cap() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_a, + codex_home.join("workspace-a"), + )) + .await + .expect("upsert thread a"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_b, + codex_home.join("workspace-b"), + )) + .await + .expect("upsert thread b"); + + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let runtime_a = Arc::clone(&runtime); + let runtime_b = Arc::clone(&runtime); + + let (claim_a, claim_b) = tokio::join!( + async move { + runtime_a + .try_claim_stage1_job(thread_a, owner_a, 100, 3_600, 1) + .await + .expect("claim stage1 thread a") + }, + async move { + runtime_b + .try_claim_stage1_job(thread_b, owner_b, 101, 3_600, 1) + .await + .expect("claim stage1 thread b") + }, + ); + + let claim_outcomes = vec![claim_a, claim_b]; + let claimed_count = claim_outcomes + .iter() + .filter(|outcome| matches!(outcome, Stage1JobClaimOutcome::Claimed { .. })) + .count(); + assert_eq!(claimed_count, 1); + assert!( + claim_outcomes + .iter() + .any(|outcome| { matches!(outcome, Stage1JobClaimOutcome::SkippedRunning) }), + "one concurrent claim should be throttled by running cap: {claim_outcomes:?}" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn claim_stage1_jobs_filters_by_age_idle_and_current_thread() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let now = Utc::now(); + let fresh_at = now - Duration::hours(1); + let just_under_idle_at = now - Duration::hours(12) + Duration::minutes(1); + let eligible_idle_at = now - Duration::hours(12) - Duration::minutes(1); + let old_at = now - Duration::days(31); + + let current_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); + let fresh_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("fresh thread id"); + let just_under_idle_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("just under idle thread id"); + let eligible_idle_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("eligible idle thread id"); + let old_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("old thread id"); + + let mut current = + test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); + current.created_at = now; + current.updated_at = now; + runtime + .upsert_thread(¤t) + .await + .expect("upsert current"); + + let mut fresh = + test_thread_metadata(&codex_home, fresh_thread_id, codex_home.join("fresh")); + fresh.created_at = fresh_at; + fresh.updated_at = fresh_at; + runtime.upsert_thread(&fresh).await.expect("upsert fresh"); + + let mut just_under_idle = test_thread_metadata( + &codex_home, + just_under_idle_thread_id, + codex_home.join("just-under-idle"), + ); + just_under_idle.created_at = just_under_idle_at; + just_under_idle.updated_at = just_under_idle_at; + runtime + .upsert_thread(&just_under_idle) + .await + .expect("upsert just-under-idle"); + + let mut eligible_idle = test_thread_metadata( + &codex_home, + eligible_idle_thread_id, + codex_home.join("eligible-idle"), + ); + eligible_idle.created_at = eligible_idle_at; + eligible_idle.updated_at = eligible_idle_at; + runtime + .upsert_thread(&eligible_idle) + .await + .expect("upsert eligible-idle"); + + let mut old = test_thread_metadata(&codex_home, old_thread_id, codex_home.join("old")); + old.created_at = old_at; + old.updated_at = old_at; + runtime.upsert_thread(&old).await.expect("upsert old"); + + let allowed_sources = vec!["cli".to_string()]; + let claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 1, + max_claimed: 5, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3600, + }, + ) + .await + .expect("claim stage1 jobs"); + + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].thread.id, eligible_idle_thread_id); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn claim_stage1_jobs_prefilters_threads_with_up_to_date_memory() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let now = Utc::now(); + let eligible_newer_at = now - Duration::hours(13); + let eligible_older_at = now - Duration::hours(14); + + let current_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); + let up_to_date_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("up-to-date thread id"); + let stale_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("stale thread id"); + let worker_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("worker id"); + + let mut current = + test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); + current.created_at = now; + current.updated_at = now; + runtime + .upsert_thread(¤t) + .await + .expect("upsert current thread"); + + let mut up_to_date = test_thread_metadata( + &codex_home, + up_to_date_thread_id, + codex_home.join("up-to-date"), + ); + up_to_date.created_at = eligible_newer_at; + up_to_date.updated_at = eligible_newer_at; + runtime + .upsert_thread(&up_to_date) + .await + .expect("upsert up-to-date thread"); + + let up_to_date_claim = runtime + .try_claim_stage1_job( + up_to_date_thread_id, + worker_id, + up_to_date.updated_at.timestamp(), + 3600, + 64, + ) + .await + .expect("claim up-to-date thread for seed"); + let up_to_date_token = match up_to_date_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected seed claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + up_to_date_thread_id, + up_to_date_token.as_str(), + up_to_date.updated_at.timestamp(), + "raw", + "summary", + None, + ) + .await + .expect("mark up-to-date thread succeeded"), + "seed stage1 success should complete for up-to-date thread" + ); + + let mut stale = + test_thread_metadata(&codex_home, stale_thread_id, codex_home.join("stale")); + stale.created_at = eligible_older_at; + stale.updated_at = eligible_older_at; + runtime + .upsert_thread(&stale) + .await + .expect("upsert stale thread"); + + let allowed_sources = vec!["cli".to_string()]; + let claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 1, + max_claimed: 1, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3600, + }, + ) + .await + .expect("claim stage1 startup jobs"); + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].thread.id, stale_thread_id); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn claim_stage1_jobs_skips_threads_with_disabled_memory_mode() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let now = Utc::now(); + let eligible_at = now - Duration::hours(13); + + let current_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); + let disabled_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("disabled thread id"); + let enabled_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("enabled thread id"); + + let mut current = + test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); + current.created_at = now; + current.updated_at = now; + runtime + .upsert_thread(¤t) + .await + .expect("upsert current thread"); + + let mut disabled = + test_thread_metadata(&codex_home, disabled_thread_id, codex_home.join("disabled")); + disabled.created_at = eligible_at; + disabled.updated_at = eligible_at; + runtime + .upsert_thread(&disabled) + .await + .expect("upsert disabled thread"); + sqlx::query("UPDATE threads SET memory_mode = 'disabled' WHERE id = ?") + .bind(disabled_thread_id.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("disable thread memory mode"); + + let mut enabled = + test_thread_metadata(&codex_home, enabled_thread_id, codex_home.join("enabled")); + enabled.created_at = eligible_at; + enabled.updated_at = eligible_at; + runtime + .upsert_thread(&enabled) + .await + .expect("upsert enabled thread"); + + let allowed_sources = vec!["cli".to_string()]; + let claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 10, + max_claimed: 10, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3600, + }, + ) + .await + .expect("claim stage1 startup jobs"); + + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].thread.id, enabled_thread_id); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn claim_stage1_jobs_enforces_global_running_cap() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let current_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + current_thread_id, + codex_home.join("current"), + )) + .await + .expect("upsert current"); + + let now = Utc::now(); + let started_at = now.timestamp(); + let lease_until = started_at + 3600; + let eligible_at = now - Duration::hours(13); + let existing_running = 10usize; + let total_candidates = 80usize; + + for idx in 0..total_candidates { + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let mut metadata = test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(format!("thread-{idx}")), + ); + metadata.created_at = eligible_at - Duration::seconds(idx as i64); + metadata.updated_at = eligible_at - Duration::seconds(idx as i64); + runtime + .upsert_thread(&metadata) + .await + .expect("upsert thread"); + + if idx < existing_running { + sqlx::query( + r#" +INSERT INTO jobs ( + kind, + job_key, + status, + worker_id, + ownership_token, + started_at, + finished_at, + lease_until, + retry_at, + retry_remaining, + last_error, + input_watermark, + last_success_watermark +) VALUES (?, ?, 'running', ?, ?, ?, NULL, ?, NULL, ?, NULL, ?, NULL) + "#, + ) + .bind("memory_stage1") + .bind(thread_id.to_string()) + .bind(current_thread_id.to_string()) + .bind(Uuid::new_v4().to_string()) + .bind(started_at) + .bind(lease_until) + .bind(3) + .bind(metadata.updated_at.timestamp()) + .execute(runtime.pool.as_ref()) + .await + .expect("seed running stage1 job"); + } + } + + let allowed_sources = vec!["cli".to_string()]; + let claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 200, + max_claimed: 64, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3600, + }, + ) + .await + .expect("claim stage1 jobs"); + assert_eq!(claims.len(), 54); + + let running_count = sqlx::query( + r#" +SELECT COUNT(*) AS count +FROM jobs +WHERE kind = 'memory_stage1' + AND status = 'running' + AND lease_until IS NOT NULL + AND lease_until > ? + "#, + ) + .bind(Utc::now().timestamp()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("count running stage1 jobs") + .try_get::("count") + .expect("running count value"); + assert_eq!(running_count, 64); + + let more_claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 200, + max_claimed: 64, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3600, + }, + ) + .await + .expect("claim stage1 jobs with cap reached"); + assert_eq!(more_claims.len(), 0); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn claim_stage1_jobs_processes_two_full_batches_across_startup_passes() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let current_thread_id = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("current thread id"); + let mut current = + test_thread_metadata(&codex_home, current_thread_id, codex_home.join("current")); + current.created_at = Utc::now(); + current.updated_at = Utc::now(); + runtime + .upsert_thread(¤t) + .await + .expect("upsert current"); + + let eligible_at = Utc::now() - Duration::hours(13); + for idx in 0..200 { + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let mut metadata = test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(format!("thread-{idx}")), + ); + metadata.created_at = eligible_at - Duration::seconds(idx as i64); + metadata.updated_at = eligible_at - Duration::seconds(idx as i64); + runtime + .upsert_thread(&metadata) + .await + .expect("upsert eligible thread"); + } + + let allowed_sources = vec!["cli".to_string()]; + let first_claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 5_000, + max_claimed: 64, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3_600, + }, + ) + .await + .expect("first stage1 startup claim"); + assert_eq!(first_claims.len(), 64); + + for claim in first_claims { + assert!( + runtime + .mark_stage1_job_succeeded( + claim.thread.id, + claim.ownership_token.as_str(), + claim.thread.updated_at.timestamp(), + "raw", + "summary", + None, + ) + .await + .expect("mark first-batch stage1 success"), + "first batch stage1 completion should succeed" + ); + } + + let second_claims = runtime + .claim_stage1_jobs_for_startup( + current_thread_id, + Stage1StartupClaimParams { + scan_limit: 5_000, + max_claimed: 64, + max_age_days: 30, + min_rollout_idle_hours: 12, + allowed_sources: allowed_sources.as_slice(), + lease_seconds: 3_600, + }, + ) + .await + .expect("second stage1 startup claim"); + assert_eq!(second_claims.len(), 64); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn stage1_output_cascades_on_thread_delete() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let cwd = codex_home.join("workspace"); + runtime + .upsert_thread(&test_thread_metadata(&codex_home, thread_id, cwd)) + .await + .expect("upsert thread"); + + let claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + 100, + "raw", + "sum", + None, + ) + .await + .expect("mark stage1 succeeded"), + "mark stage1 succeeded should write stage1_outputs" + ); + + let count_before = + sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("count before delete") + .try_get::("count") + .expect("count value"); + assert_eq!(count_before, 1); + + sqlx::query("DELETE FROM threads WHERE id = ?") + .bind(thread_id.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("delete thread"); + + let count_after = + sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("count after delete") + .try_get::("count") + .expect("count value"); + assert_eq!(count_after, 0); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn mark_stage1_job_succeeded_no_output_skips_phase2_when_output_was_already_absent() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded_no_output(thread_id, ownership_token.as_str()) + .await + .expect("mark stage1 succeeded without output"), + "stage1 no-output success should complete the job" + ); + + let output_row_count = + sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load stage1 output count") + .try_get::("count") + .expect("stage1 output count"); + assert_eq!( + output_row_count, 0, + "stage1 no-output success should not persist empty stage1 outputs" + ); + + let up_to_date = runtime + .try_claim_stage1_job(thread_id, owner_b, 100, 3600, 64) + .await + .expect("claim stage1 up-to-date"); + assert_eq!(up_to_date, Stage1JobClaimOutcome::SkippedUpToDate); + + let global_job_row_count = sqlx::query("SELECT COUNT(*) AS count FROM jobs WHERE kind = ?") + .bind("memory_consolidate_global") + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load phase2 job row count") + .try_get::("count") + .expect("phase2 job row count"); + assert_eq!( + global_job_row_count, 0, + "no-output without an existing stage1 output should not enqueue phase2" + ); + + let claim_phase2 = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + assert_eq!( + claim_phase2, + Phase2JobClaimOutcome::SkippedNotDirty, + "phase2 should remain clean when no-output deleted nothing" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn mark_stage1_job_succeeded_no_output_enqueues_phase2_when_deleting_output() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let first_claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim initial stage1"); + let first_token = match first_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected initial stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded(thread_id, first_token.as_str(), 100, "raw", "sum", None) + .await + .expect("mark initial stage1 succeeded"), + "initial stage1 success should create stage1 output" + ); + + let phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2 after initial output"); + let (phase2_token, phase2_input_watermark) = match phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim after initial output: {other:?}"), + }; + assert_eq!(phase2_input_watermark, 100); + assert!( + runtime + .mark_global_phase2_job_succeeded( + phase2_token.as_str(), + phase2_input_watermark, + &[], + ) + .await + .expect("mark initial phase2 succeeded"), + "initial phase2 success should clear global dirty state" + ); + + let no_output_claim = runtime + .try_claim_stage1_job(thread_id, owner_b, 101, 3600, 64) + .await + .expect("claim stage1 for no-output delete"); + let no_output_token = match no_output_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected no-output stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded_no_output(thread_id, no_output_token.as_str()) + .await + .expect("mark stage1 no-output after existing output"), + "no-output should succeed when deleting an existing stage1 output" + ); + + let output_row_count = + sqlx::query("SELECT COUNT(*) AS count FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load stage1 output count after delete") + .try_get::("count") + .expect("stage1 output count"); + assert_eq!(output_row_count, 0); + + let claim_phase2 = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2 after no-output deletion"); + let (phase2_token, phase2_input_watermark) = match claim_phase2 { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim after no-output deletion: {other:?}"), + }; + assert_eq!(phase2_input_watermark, 101); + assert!( + runtime + .mark_global_phase2_job_succeeded( + phase2_token.as_str(), + phase2_input_watermark, + &[], + ) + .await + .expect("mark phase2 succeeded after no-output delete") + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn stage1_retry_exhaustion_does_not_block_newer_watermark() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + for attempt in 0..3 { + let claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) + .await + .expect("claim stage1 for retry exhaustion"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!( + "attempt {} should claim stage1 before retries are exhausted: {other:?}", + attempt + 1 + ), + }; + assert!( + runtime + .mark_stage1_job_failed(thread_id, ownership_token.as_str(), "boom", 0) + .await + .expect("mark stage1 failed"), + "attempt {} should decrement retry budget", + attempt + 1 + ); + } + + let exhausted_claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3_600, 64) + .await + .expect("claim stage1 after retry exhaustion"); + assert_eq!( + exhausted_claim, + Stage1JobClaimOutcome::SkippedRetryExhausted + ); + + let newer_source_claim = runtime + .try_claim_stage1_job(thread_id, owner, 101, 3_600, 64) + .await + .expect("claim stage1 with newer source watermark"); + assert!( + matches!(newer_source_claim, Stage1JobClaimOutcome::Claimed { .. }), + "newer source watermark should reset retry budget and be claimable" + ); + + let job_row = sqlx::query( + "SELECT retry_remaining, input_watermark FROM jobs WHERE kind = ? AND job_key = ?", + ) + .bind("memory_stage1") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load stage1 job row after newer-source claim"); + assert_eq!( + job_row + .try_get::("retry_remaining") + .expect("retry_remaining"), + 3 + ); + assert_eq!( + job_row + .try_get::("input_watermark") + .expect("input_watermark"), + 101 + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn phase2_global_consolidation_reruns_when_watermark_advances() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + runtime + .enqueue_global_consolidation(100) + .await + .expect("enqueue global consolidation"); + + let claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + let (ownership_token, input_watermark) = match claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_global_phase2_job_succeeded(ownership_token.as_str(), input_watermark, &[],) + .await + .expect("mark phase2 succeeded"), + "phase2 success should finalize for current token" + ); + + let claim_up_to_date = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2 up-to-date"); + assert_eq!(claim_up_to_date, Phase2JobClaimOutcome::SkippedNotDirty); + + runtime + .enqueue_global_consolidation(101) + .await + .expect("enqueue global consolidation again"); + + let claim_rerun = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2 rerun"); + assert!( + matches!(claim_rerun, Phase2JobClaimOutcome::Claimed { .. }), + "advanced watermark should be claimable" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn list_stage1_outputs_for_global_returns_latest_outputs() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id_a, + codex_home.join("workspace-a"), + )) + .await + .expect("upsert thread a"); + let mut metadata_b = + test_thread_metadata(&codex_home, thread_id_b, codex_home.join("workspace-b")); + metadata_b.git_branch = Some("feature/stage1-b".to_string()); + runtime + .upsert_thread(&metadata_b) + .await + .expect("upsert thread b"); + + let claim = runtime + .try_claim_stage1_job(thread_id_a, owner, 100, 3600, 64) + .await + .expect("claim stage1 a"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id_a, + ownership_token.as_str(), + 100, + "raw memory a", + "summary a", + None, + ) + .await + .expect("mark stage1 succeeded a"), + "stage1 success should persist output a" + ); + + let claim = runtime + .try_claim_stage1_job(thread_id_b, owner, 101, 3600, 64) + .await + .expect("claim stage1 b"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id_b, + ownership_token.as_str(), + 101, + "raw memory b", + "summary b", + Some("rollout-b"), + ) + .await + .expect("mark stage1 succeeded b"), + "stage1 success should persist output b" + ); + + let outputs = runtime + .list_stage1_outputs_for_global(10) + .await + .expect("list stage1 outputs for global"); + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].thread_id, thread_id_b); + assert_eq!(outputs[0].rollout_summary, "summary b"); + assert_eq!(outputs[0].rollout_slug.as_deref(), Some("rollout-b")); + assert_eq!(outputs[0].cwd, codex_home.join("workspace-b")); + assert_eq!(outputs[0].git_branch.as_deref(), Some("feature/stage1-b")); + assert_eq!(outputs[1].thread_id, thread_id_a); + assert_eq!(outputs[1].rollout_summary, "summary a"); + assert_eq!(outputs[1].rollout_slug, None); + assert_eq!(outputs[1].cwd, codex_home.join("workspace-a")); + assert_eq!(outputs[1].git_branch, None); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn list_stage1_outputs_for_global_skips_empty_payloads() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id_non_empty = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let thread_id_empty = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id_non_empty, + codex_home.join("workspace-non-empty"), + )) + .await + .expect("upsert non-empty thread"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id_empty, + codex_home.join("workspace-empty"), + )) + .await + .expect("upsert empty thread"); + + sqlx::query( + r#" +INSERT INTO stage1_outputs (thread_id, source_updated_at, raw_memory, rollout_summary, generated_at) +VALUES (?, ?, ?, ?, ?) + "#, + ) + .bind(thread_id_non_empty.to_string()) + .bind(100_i64) + .bind("raw memory") + .bind("summary") + .bind(100_i64) + .execute(runtime.pool.as_ref()) + .await + .expect("insert non-empty stage1 output"); + sqlx::query( + r#" +INSERT INTO stage1_outputs (thread_id, source_updated_at, raw_memory, rollout_summary, generated_at) +VALUES (?, ?, ?, ?, ?) + "#, + ) + .bind(thread_id_empty.to_string()) + .bind(101_i64) + .bind("") + .bind("") + .bind(101_i64) + .execute(runtime.pool.as_ref()) + .await + .expect("insert empty stage1 output"); + + let outputs = runtime + .list_stage1_outputs_for_global(1) + .await + .expect("list stage1 outputs for global"); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].thread_id, thread_id_non_empty); + assert_eq!(outputs[0].rollout_summary, "summary"); + assert_eq!(outputs[0].cwd, codex_home.join("workspace-non-empty")); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_reports_added_retained_and_removed_rows() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let thread_id_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + for (thread_id, workspace) in [ + (thread_id_a, "workspace-a"), + (thread_id_b, "workspace-b"), + (thread_id_c, "workspace-c"), + ] { + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(workspace), + )) + .await + .expect("upsert thread"); + } + + for (thread_id, updated_at, slug) in [ + (thread_id_a, 100, Some("rollout-a")), + (thread_id_b, 101, Some("rollout-b")), + (thread_id_c, 102, Some("rollout-c")), + ] { + let claim = runtime + .try_claim_stage1_job(thread_id, owner, updated_at, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + updated_at, + &format!("raw-{updated_at}"), + &format!("summary-{updated_at}"), + slug, + ) + .await + .expect("mark stage1 succeeded"), + "stage1 success should persist output" + ); + } + + let claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + let (ownership_token, input_watermark) = match claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim outcome: {other:?}"), + }; + assert_eq!(input_watermark, 102); + let selected_outputs = runtime + .list_stage1_outputs_for_global(10) + .await + .expect("list stage1 outputs for global") + .into_iter() + .filter(|output| output.thread_id == thread_id_c || output.thread_id == thread_id_a) + .collect::>(); + assert!( + runtime + .mark_global_phase2_job_succeeded( + ownership_token.as_str(), + input_watermark, + &selected_outputs, + ) + .await + .expect("mark phase2 success with selection"), + "phase2 success should persist selected rows" + ); + + let selection = runtime + .get_phase2_input_selection(2, 36_500) + .await + .expect("load phase2 input selection"); + + assert_eq!(selection.selected.len(), 2); + assert_eq!(selection.previous_selected.len(), 2); + assert_eq!(selection.selected[0].thread_id, thread_id_c); + assert_eq!( + selection.selected[0].rollout_path, + codex_home.join(format!("rollout-{thread_id_c}.jsonl")) + ); + assert_eq!(selection.selected[1].thread_id, thread_id_b); + assert_eq!(selection.retained_thread_ids, vec![thread_id_c]); + + assert_eq!(selection.removed.len(), 1); + assert_eq!(selection.removed[0].thread_id, thread_id_a); + assert_eq!( + selection.removed[0].rollout_slug.as_deref(), + Some("rollout-a") + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_treats_regenerated_selected_rows_as_added() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let first_claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim initial stage1"); + let first_token = match first_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + first_token.as_str(), + 100, + "raw-100", + "summary-100", + Some("rollout-100"), + ) + .await + .expect("mark initial stage1 success"), + "initial stage1 success should persist output" + ); + + let phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + let (phase2_token, input_watermark) = match phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim outcome: {other:?}"), + }; + let selected_outputs = runtime + .list_stage1_outputs_for_global(1) + .await + .expect("list selected outputs"); + assert!( + runtime + .mark_global_phase2_job_succeeded( + phase2_token.as_str(), + input_watermark, + &selected_outputs, + ) + .await + .expect("mark phase2 success"), + "phase2 success should persist selected rows" + ); + + let refreshed_claim = runtime + .try_claim_stage1_job(thread_id, owner, 101, 3600, 64) + .await + .expect("claim refreshed stage1"); + let refreshed_token = match refreshed_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + refreshed_token.as_str(), + 101, + "raw-101", + "summary-101", + Some("rollout-101"), + ) + .await + .expect("mark refreshed stage1 success"), + "refreshed stage1 success should persist output" + ); + + let selection = runtime + .get_phase2_input_selection(1, 36_500) + .await + .expect("load phase2 input selection"); + assert_eq!(selection.selected.len(), 1); + assert_eq!(selection.previous_selected.len(), 1); + assert_eq!(selection.selected[0].thread_id, thread_id); + assert_eq!(selection.selected[0].source_updated_at.timestamp(), 101); + assert!(selection.retained_thread_ids.is_empty()); + assert!(selection.removed.is_empty()); + + let (selected_for_phase2, selected_for_phase2_source_updated_at) = + sqlx::query_as::<_, (i64, Option)>( + "SELECT selected_for_phase2, selected_for_phase2_source_updated_at FROM stage1_outputs WHERE thread_id = ?", + ) + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load selected_for_phase2"); + assert_eq!(selected_for_phase2, 1); + assert_eq!(selected_for_phase2_source_updated_at, Some(100)); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_reports_regenerated_previous_selection_as_removed() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread a"); + let thread_id_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread b"); + let thread_id_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread c"); + let thread_id_d = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread d"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + for (thread_id, workspace) in [ + (thread_id_a, "workspace-a"), + (thread_id_b, "workspace-b"), + (thread_id_c, "workspace-c"), + (thread_id_d, "workspace-d"), + ] { + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(workspace), + )) + .await + .expect("upsert thread"); + } + + for (thread_id, updated_at, slug) in [ + (thread_id_a, 100, Some("rollout-a-100")), + (thread_id_b, 101, Some("rollout-b-101")), + (thread_id_c, 99, Some("rollout-c-99")), + (thread_id_d, 98, Some("rollout-d-98")), + ] { + let claim = runtime + .try_claim_stage1_job(thread_id, owner, updated_at, 3600, 64) + .await + .expect("claim initial stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + updated_at, + &format!("raw-{updated_at}"), + &format!("summary-{updated_at}"), + slug, + ) + .await + .expect("mark stage1 succeeded"), + "stage1 success should persist output" + ); + } + + let phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + let (phase2_token, input_watermark) = match phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim outcome: {other:?}"), + }; + let selected_outputs = runtime + .list_stage1_outputs_for_global(2) + .await + .expect("list selected outputs"); + assert_eq!( + selected_outputs + .iter() + .map(|output| output.thread_id) + .collect::>(), + vec![thread_id_b, thread_id_a] + ); + assert!( + runtime + .mark_global_phase2_job_succeeded( + phase2_token.as_str(), + input_watermark, + &selected_outputs, + ) + .await + .expect("mark phase2 success"), + "phase2 success should persist selected rows" + ); + + for (thread_id, updated_at, slug) in [ + (thread_id_a, 102, Some("rollout-a-102")), + (thread_id_c, 103, Some("rollout-c-103")), + (thread_id_d, 104, Some("rollout-d-104")), + ] { + let claim = runtime + .try_claim_stage1_job(thread_id, owner, updated_at, 3600, 64) + .await + .expect("claim refreshed stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + updated_at, + &format!("raw-{updated_at}"), + &format!("summary-{updated_at}"), + slug, + ) + .await + .expect("mark refreshed stage1 success"), + "refreshed stage1 success should persist output" + ); + } + + let selection = runtime + .get_phase2_input_selection(2, 36_500) + .await + .expect("load phase2 input selection"); + assert_eq!( + selection + .selected + .iter() + .map(|output| output.thread_id) + .collect::>(), + vec![thread_id_d, thread_id_c] + ); + assert_eq!( + selection + .previous_selected + .iter() + .map(|output| output.thread_id) + .collect::>(), + vec![thread_id_a, thread_id_b] + ); + assert!(selection.retained_thread_ids.is_empty()); + assert_eq!( + selection + .removed + .iter() + .map(|output| (output.thread_id, output.source_updated_at.timestamp())) + .collect::>(), + vec![(thread_id_a, 102), (thread_id_b, 101)] + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn mark_global_phase2_job_succeeded_updates_selected_snapshot_timestamp() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let initial_claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim initial stage1"); + let initial_token = match initial_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + initial_token.as_str(), + 100, + "raw-100", + "summary-100", + Some("rollout-100"), + ) + .await + .expect("mark initial stage1 success"), + "initial stage1 success should persist output" + ); + + let first_phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim first phase2"); + let (first_phase2_token, first_input_watermark) = match first_phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected first phase2 claim outcome: {other:?}"), + }; + let first_selected_outputs = runtime + .list_stage1_outputs_for_global(1) + .await + .expect("list first selected outputs"); + assert!( + runtime + .mark_global_phase2_job_succeeded( + first_phase2_token.as_str(), + first_input_watermark, + &first_selected_outputs, + ) + .await + .expect("mark first phase2 success"), + "first phase2 success should persist selected rows" + ); + + let refreshed_claim = runtime + .try_claim_stage1_job(thread_id, owner, 101, 3600, 64) + .await + .expect("claim refreshed stage1"); + let refreshed_token = match refreshed_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected refreshed stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + refreshed_token.as_str(), + 101, + "raw-101", + "summary-101", + Some("rollout-101"), + ) + .await + .expect("mark refreshed stage1 success"), + "refreshed stage1 success should persist output" + ); + + let second_phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim second phase2"); + let (second_phase2_token, second_input_watermark) = match second_phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected second phase2 claim outcome: {other:?}"), + }; + let second_selected_outputs = runtime + .list_stage1_outputs_for_global(1) + .await + .expect("list second selected outputs"); + assert_eq!( + second_selected_outputs[0].source_updated_at.timestamp(), + 101 + ); + assert!( + runtime + .mark_global_phase2_job_succeeded( + second_phase2_token.as_str(), + second_input_watermark, + &second_selected_outputs, + ) + .await + .expect("mark second phase2 success"), + "second phase2 success should persist selected rows" + ); + + let selection = runtime + .get_phase2_input_selection(1, 36_500) + .await + .expect("load phase2 input selection after refresh"); + assert_eq!(selection.retained_thread_ids, vec![thread_id]); + + let (selected_for_phase2, selected_for_phase2_source_updated_at) = + sqlx::query_as::<_, (i64, Option)>( + "SELECT selected_for_phase2, selected_for_phase2_source_updated_at FROM stage1_outputs WHERE thread_id = ?", + ) + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load selected snapshot after phase2"); + assert_eq!(selected_for_phase2, 1); + assert_eq!(selected_for_phase2_source_updated_at, Some(101)); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn mark_global_phase2_job_succeeded_only_marks_exact_selected_snapshots() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_id = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join("workspace"), + )) + .await + .expect("upsert thread"); + + let initial_claim = runtime + .try_claim_stage1_job(thread_id, owner, 100, 3600, 64) + .await + .expect("claim initial stage1"); + let initial_token = match initial_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + initial_token.as_str(), + 100, + "raw-100", + "summary-100", + Some("rollout-100"), + ) + .await + .expect("mark initial stage1 success"), + "initial stage1 success should persist output" + ); + + let phase2_claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim phase2"); + let (phase2_token, input_watermark) = match phase2_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected phase2 claim outcome: {other:?}"), + }; + let selected_outputs = runtime + .list_stage1_outputs_for_global(1) + .await + .expect("list selected outputs"); + assert_eq!(selected_outputs[0].source_updated_at.timestamp(), 100); + + let refreshed_claim = runtime + .try_claim_stage1_job(thread_id, owner, 101, 3600, 64) + .await + .expect("claim refreshed stage1"); + let refreshed_token = match refreshed_claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + refreshed_token.as_str(), + 101, + "raw-101", + "summary-101", + Some("rollout-101"), + ) + .await + .expect("mark refreshed stage1 success"), + "refreshed stage1 success should persist output" + ); + + assert!( + runtime + .mark_global_phase2_job_succeeded( + phase2_token.as_str(), + input_watermark, + &selected_outputs, + ) + .await + .expect("mark phase2 success"), + "phase2 success should still complete" + ); + + let (selected_for_phase2, selected_for_phase2_source_updated_at) = + sqlx::query_as::<_, (i64, Option)>( + "SELECT selected_for_phase2, selected_for_phase2_source_updated_at FROM stage1_outputs WHERE thread_id = ?", + ) + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load selected_for_phase2"); + assert_eq!(selected_for_phase2, 0); + assert_eq!(selected_for_phase2_source_updated_at, None); + + let selection = runtime + .get_phase2_input_selection(1, 36_500) + .await + .expect("load phase2 input selection"); + assert_eq!(selection.selected.len(), 1); + assert_eq!(selection.selected[0].source_updated_at.timestamp(), 101); + assert!(selection.retained_thread_ids.is_empty()); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn record_stage1_output_usage_updates_usage_metadata() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a"); + let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b"); + let missing = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("missing id"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_a, + codex_home.join("workspace-a"), + )) + .await + .expect("upsert thread a"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_b, + codex_home.join("workspace-b"), + )) + .await + .expect("upsert thread b"); + + let claim_a = runtime + .try_claim_stage1_job(thread_a, owner, 100, 3600, 64) + .await + .expect("claim stage1 a"); + let token_a = match claim_a { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome for a: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded(thread_a, token_a.as_str(), 100, "raw a", "sum a", None) + .await + .expect("mark stage1 succeeded a") + ); + + let claim_b = runtime + .try_claim_stage1_job(thread_b, owner, 101, 3600, 64) + .await + .expect("claim stage1 b"); + let token_b = match claim_b { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome for b: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded(thread_b, token_b.as_str(), 101, "raw b", "sum b", None) + .await + .expect("mark stage1 succeeded b") + ); + + let updated_rows = runtime + .record_stage1_output_usage(&[thread_a, thread_a, thread_b, missing]) + .await + .expect("record stage1 output usage"); + assert_eq!(updated_rows, 3); + + let row_a = + sqlx::query("SELECT usage_count, last_usage FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_a.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load stage1 usage row a"); + let row_b = + sqlx::query("SELECT usage_count, last_usage FROM stage1_outputs WHERE thread_id = ?") + .bind(thread_b.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("load stage1 usage row b"); + + assert_eq!( + row_a + .try_get::("usage_count") + .expect("usage_count a"), + 2 + ); + assert_eq!( + row_b + .try_get::("usage_count") + .expect("usage_count b"), + 1 + ); + + let last_usage_a = row_a.try_get::("last_usage").expect("last_usage a"); + let last_usage_b = row_b.try_get::("last_usage").expect("last_usage b"); + assert_eq!(last_usage_a, last_usage_b); + assert!(last_usage_a > 0); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_prioritizes_usage_count_then_recent_usage() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let now = Utc::now(); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a"); + let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b"); + let thread_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id c"); + + for (thread_id, workspace) in [ + (thread_a, "workspace-a"), + (thread_b, "workspace-b"), + (thread_c, "workspace-c"), + ] { + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(workspace), + )) + .await + .expect("upsert thread"); + } + + for (thread_id, generated_at, summary) in [ + (thread_a, now - Duration::days(3), "summary-a"), + (thread_b, now - Duration::days(2), "summary-b"), + (thread_c, now - Duration::days(1), "summary-c"), + ] { + let source_updated_at = generated_at.timestamp(); + let claim = runtime + .try_claim_stage1_job(thread_id, owner, source_updated_at, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + source_updated_at, + &format!("raw-{summary}"), + summary, + None, + ) + .await + .expect("mark stage1 success"), + "stage1 success should persist output" + ); + } + + for (thread_id, usage_count, last_usage) in [ + (thread_a, 5_i64, now - Duration::days(10)), + (thread_b, 5_i64, now - Duration::days(1)), + (thread_c, 1_i64, now - Duration::hours(1)), + ] { + sqlx::query( + "UPDATE stage1_outputs SET usage_count = ?, last_usage = ? WHERE thread_id = ?", + ) + .bind(usage_count) + .bind(last_usage.timestamp()) + .bind(thread_id.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("update usage metadata"); + } + + let selection = runtime + .get_phase2_input_selection(3, 30) + .await + .expect("load phase2 input selection"); + + assert_eq!( + selection + .selected + .iter() + .map(|output| output.thread_id) + .collect::>(), + vec![thread_b, thread_a, thread_c] + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_excludes_stale_used_memories_but_keeps_fresh_never_used() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let now = Utc::now(); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a"); + let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b"); + let thread_c = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id c"); + + for (thread_id, workspace) in [ + (thread_a, "workspace-a"), + (thread_b, "workspace-b"), + (thread_c, "workspace-c"), + ] { + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(workspace), + )) + .await + .expect("upsert thread"); + } + + for (thread_id, generated_at, summary) in [ + (thread_a, now - Duration::days(40), "summary-a"), + (thread_b, now - Duration::days(2), "summary-b"), + (thread_c, now - Duration::days(50), "summary-c"), + ] { + let source_updated_at = generated_at.timestamp(); + let claim = runtime + .try_claim_stage1_job(thread_id, owner, source_updated_at, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + source_updated_at, + &format!("raw-{summary}"), + summary, + None, + ) + .await + .expect("mark stage1 success"), + "stage1 success should persist output" + ); + } + + for (thread_id, usage_count, last_usage) in [ + (thread_a, Some(9_i64), Some(now - Duration::days(31))), + (thread_b, None, None), + (thread_c, Some(1_i64), Some(now - Duration::days(1))), + ] { + sqlx::query( + "UPDATE stage1_outputs SET usage_count = ?, last_usage = ? WHERE thread_id = ?", + ) + .bind(usage_count) + .bind(last_usage.map(|value| value.timestamp())) + .bind(thread_id.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("update usage metadata"); + } + + let selection = runtime + .get_phase2_input_selection(3, 30) + .await + .expect("load phase2 input selection"); + + assert_eq!( + selection + .selected + .iter() + .map(|output| output.thread_id) + .collect::>(), + vec![thread_c, thread_b] + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn get_phase2_input_selection_prefers_recent_thread_updates_over_recent_generation() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + let older_thread = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("older thread id"); + let newer_thread = + ThreadId::from_string(&Uuid::new_v4().to_string()).expect("newer thread id"); + + for (thread_id, workspace) in [ + (older_thread, "workspace-older"), + (newer_thread, "workspace-newer"), + ] { + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_id, + codex_home.join(workspace), + )) + .await + .expect("upsert thread"); + } + + for (thread_id, source_updated_at, summary) in [ + (older_thread, 100_i64, "summary-older"), + (newer_thread, 200_i64, "summary-newer"), + ] { + let claim = runtime + .try_claim_stage1_job(thread_id, owner, source_updated_at, 3600, 64) + .await + .expect("claim stage1"); + let ownership_token = match claim { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_id, + ownership_token.as_str(), + source_updated_at, + &format!("raw-{summary}"), + summary, + None, + ) + .await + .expect("mark stage1 success"), + "stage1 success should persist output" + ); + } + + sqlx::query("UPDATE stage1_outputs SET generated_at = ? WHERE thread_id = ?") + .bind(300_i64) + .bind(older_thread.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("update older generated_at"); + sqlx::query("UPDATE stage1_outputs SET generated_at = ? WHERE thread_id = ?") + .bind(150_i64) + .bind(newer_thread.to_string()) + .execute(runtime.pool.as_ref()) + .await + .expect("update newer generated_at"); + + let selection = runtime + .get_phase2_input_selection(1, 36_500) + .await + .expect("load phase2 input selection"); + + assert_eq!(selection.selected.len(), 1); + assert_eq!(selection.selected[0].thread_id, newer_thread); + assert_eq!(selection.selected[0].source_updated_at.timestamp(), 200); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn mark_stage1_job_succeeded_enqueues_global_consolidation() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + let thread_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id a"); + let thread_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("thread id b"); + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner id"); + + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_a, + codex_home.join("workspace-a"), + )) + .await + .expect("upsert thread a"); + runtime + .upsert_thread(&test_thread_metadata( + &codex_home, + thread_b, + codex_home.join("workspace-b"), + )) + .await + .expect("upsert thread b"); + + let claim_a = runtime + .try_claim_stage1_job(thread_a, owner, 100, 3600, 64) + .await + .expect("claim stage1 a"); + let token_a = match claim_a { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome for thread a: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_a, + token_a.as_str(), + 100, + "raw-a", + "summary-a", + None, + ) + .await + .expect("mark stage1 succeeded a"), + "stage1 success should persist output for thread a" + ); + + let claim_b = runtime + .try_claim_stage1_job(thread_b, owner, 101, 3600, 64) + .await + .expect("claim stage1 b"); + let token_b = match claim_b { + Stage1JobClaimOutcome::Claimed { ownership_token } => ownership_token, + other => panic!("unexpected stage1 claim outcome for thread b: {other:?}"), + }; + assert!( + runtime + .mark_stage1_job_succeeded( + thread_b, + token_b.as_str(), + 101, + "raw-b", + "summary-b", + None, + ) + .await + .expect("mark stage1 succeeded b"), + "stage1 success should persist output for thread b" + ); + + let claim = runtime + .try_claim_global_phase2_job(owner, 3600) + .await + .expect("claim global consolidation"); + let input_watermark = match claim { + Phase2JobClaimOutcome::Claimed { + input_watermark, .. + } => input_watermark, + other => panic!("unexpected global consolidation claim outcome: {other:?}"), + }; + assert_eq!(input_watermark, 101); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn phase2_global_lock_allows_only_one_fresh_runner() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .enqueue_global_consolidation(200) + .await + .expect("enqueue global consolidation"); + + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); + + let running_claim = runtime + .try_claim_global_phase2_job(owner_a, 3600) + .await + .expect("claim global lock"); + assert!( + matches!(running_claim, Phase2JobClaimOutcome::Claimed { .. }), + "first owner should claim global lock" + ); + + let second_claim = runtime + .try_claim_global_phase2_job(owner_b, 3600) + .await + .expect("claim global lock from second owner"); + assert_eq!(second_claim, Phase2JobClaimOutcome::SkippedRunning); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn phase2_global_lock_stale_lease_allows_takeover() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .enqueue_global_consolidation(300) + .await + .expect("enqueue global consolidation"); + + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); + + let initial_claim = runtime + .try_claim_global_phase2_job(owner_a, 3600) + .await + .expect("claim initial global lock"); + let token_a = match initial_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, .. + } => ownership_token, + other => panic!("unexpected initial claim outcome: {other:?}"), + }; + + sqlx::query("UPDATE jobs SET lease_until = ? WHERE kind = ? AND job_key = ?") + .bind(Utc::now().timestamp() - 1) + .bind("memory_consolidate_global") + .bind("global") + .execute(runtime.pool.as_ref()) + .await + .expect("expire global consolidation lease"); + + let takeover_claim = runtime + .try_claim_global_phase2_job(owner_b, 3600) + .await + .expect("claim stale global lock"); + let (token_b, input_watermark) = match takeover_claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => (ownership_token, input_watermark), + other => panic!("unexpected takeover claim outcome: {other:?}"), + }; + assert_ne!(token_a, token_b); + assert_eq!(input_watermark, 300); + + assert_eq!( + runtime + .mark_global_phase2_job_succeeded(token_a.as_str(), 300, &[]) + .await + .expect("mark stale owner success result"), + false, + "stale owner should lose finalization ownership after takeover" + ); + assert!( + runtime + .mark_global_phase2_job_succeeded(token_b.as_str(), 300, &[]) + .await + .expect("mark takeover owner success"), + "takeover owner should finalize consolidation" + ); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn phase2_backfilled_inputs_below_last_success_still_become_dirty() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .enqueue_global_consolidation(500) + .await + .expect("enqueue initial consolidation"); + let owner_a = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner a"); + let claim_a = runtime + .try_claim_global_phase2_job(owner_a, 3_600) + .await + .expect("claim initial consolidation"); + let token_a = match claim_a { + Phase2JobClaimOutcome::Claimed { + ownership_token, + input_watermark, + } => { + assert_eq!(input_watermark, 500); + ownership_token + } + other => panic!("unexpected initial phase2 claim outcome: {other:?}"), + }; + assert!( + runtime + .mark_global_phase2_job_succeeded(token_a.as_str(), 500, &[]) + .await + .expect("mark initial phase2 success"), + "initial phase2 success should finalize" + ); + + runtime + .enqueue_global_consolidation(400) + .await + .expect("enqueue backfilled consolidation"); + + let owner_b = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner b"); + let claim_b = runtime + .try_claim_global_phase2_job(owner_b, 3_600) + .await + .expect("claim backfilled consolidation"); + match claim_b { + Phase2JobClaimOutcome::Claimed { + input_watermark, .. + } => { + assert!( + input_watermark > 500, + "backfilled enqueue should advance dirty watermark beyond last success" + ); + } + other => panic!("unexpected backfilled phase2 claim outcome: {other:?}"), + } + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } + + #[tokio::test] + async fn phase2_failure_fallback_updates_unowned_running_job() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("initialize runtime"); + + runtime + .enqueue_global_consolidation(400) + .await + .expect("enqueue global consolidation"); + + let owner = ThreadId::from_string(&Uuid::new_v4().to_string()).expect("owner"); + let claim = runtime + .try_claim_global_phase2_job(owner, 3_600) + .await + .expect("claim global consolidation"); + let ownership_token = match claim { + Phase2JobClaimOutcome::Claimed { + ownership_token, .. + } => ownership_token, + other => panic!("unexpected claim outcome: {other:?}"), + }; + + sqlx::query("UPDATE jobs SET ownership_token = NULL WHERE kind = ? AND job_key = ?") + .bind("memory_consolidate_global") + .bind("global") + .execute(runtime.pool.as_ref()) + .await + .expect("clear ownership token"); + + assert_eq!( + runtime + .mark_global_phase2_job_failed(ownership_token.as_str(), "lost", 3_600) + .await + .expect("mark phase2 failed with strict ownership"), + false, + "strict failure update should not match unowned running job" + ); + assert!( + runtime + .mark_global_phase2_job_failed_if_unowned(ownership_token.as_str(), "lost", 3_600) + .await + .expect("fallback failure update should match unowned running job"), + "fallback failure update should transition the unowned running job" + ); + + let claim = runtime + .try_claim_global_phase2_job(ThreadId::new(), 3_600) + .await + .expect("claim after fallback failure"); + assert_eq!(claim, Phase2JobClaimOutcome::SkippedNotDirty); + + let _ = tokio::fs::remove_dir_all(codex_home).await; + } +} diff --git a/codex-rs/state/src/runtime/test_support.rs b/codex-rs/state/src/runtime/test_support.rs new file mode 100644 index 0000000000..d749fe2bfb --- /dev/null +++ b/codex-rs/state/src/runtime/test_support.rs @@ -0,0 +1,64 @@ +#[cfg(test)] +use chrono::DateTime; +#[cfg(test)] +use chrono::Utc; +#[cfg(test)] +use codex_protocol::ThreadId; +#[cfg(test)] +use codex_protocol::protocol::AskForApproval; +#[cfg(test)] +use codex_protocol::protocol::SandboxPolicy; +#[cfg(test)] +use std::path::Path; +#[cfg(test)] +use std::path::PathBuf; +#[cfg(test)] +use std::time::SystemTime; +#[cfg(test)] +use std::time::UNIX_EPOCH; +#[cfg(test)] +use uuid::Uuid; + +#[cfg(test)] +use crate::ThreadMetadata; + +#[cfg(test)] +pub(super) fn unique_temp_dir() -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |duration| duration.as_nanos()); + std::env::temp_dir().join(format!( + "codex-state-runtime-test-{nanos}-{}", + Uuid::new_v4() + )) +} + +#[cfg(test)] +pub(super) fn test_thread_metadata( + codex_home: &Path, + thread_id: ThreadId, + cwd: PathBuf, +) -> ThreadMetadata { + let now = DateTime::::from_timestamp(1_700_000_000, 0).expect("timestamp"); + ThreadMetadata { + id: thread_id, + rollout_path: codex_home.join(format!("rollout-{thread_id}.jsonl")), + created_at: now, + updated_at: now, + source: "cli".to_string(), + agent_nickname: None, + agent_role: None, + model_provider: "test-provider".to_string(), + cwd, + cli_version: "0.0.0".to_string(), + title: String::new(), + sandbox_policy: crate::extract::enum_to_string(&SandboxPolicy::new_read_only_policy()), + approval_mode: crate::extract::enum_to_string(&AskForApproval::OnRequest), + tokens_used: 0, + first_user_message: Some("hello".to_string()), + archived_at: None, + git_sha: None, + git_branch: None, + git_origin_url: None, + } +} diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs new file mode 100644 index 0000000000..e02cfb3e4a --- /dev/null +++ b/codex-rs/state/src/runtime/threads.rs @@ -0,0 +1,560 @@ +use super::*; + +impl StateRuntime { + pub async fn get_thread(&self, id: ThreadId) -> anyhow::Result> { + let row = sqlx::query( + r#" +SELECT + id, + rollout_path, + created_at, + updated_at, + source, + agent_nickname, + agent_role, + model_provider, + cwd, + cli_version, + title, + sandbox_policy, + approval_mode, + tokens_used, + first_user_message, + archived_at, + git_sha, + git_branch, + git_origin_url +FROM threads +WHERE id = ? + "#, + ) + .bind(id.to_string()) + .fetch_optional(self.pool.as_ref()) + .await?; + row.map(|row| ThreadRow::try_from_row(&row).and_then(ThreadMetadata::try_from)) + .transpose() + } + + /// Get dynamic tools for a thread, if present. + pub async fn get_dynamic_tools( + &self, + thread_id: ThreadId, + ) -> anyhow::Result>> { + let rows = sqlx::query( + r#" +SELECT name, description, input_schema +FROM thread_dynamic_tools +WHERE thread_id = ? +ORDER BY position ASC + "#, + ) + .bind(thread_id.to_string()) + .fetch_all(self.pool.as_ref()) + .await?; + if rows.is_empty() { + return Ok(None); + } + let mut tools = Vec::with_capacity(rows.len()); + for row in rows { + let input_schema: String = row.try_get("input_schema")?; + let input_schema = serde_json::from_str::(input_schema.as_str())?; + tools.push(DynamicToolSpec { + name: row.try_get("name")?, + description: row.try_get("description")?, + input_schema, + }); + } + Ok(Some(tools)) + } + + /// Find a rollout path by thread id using the underlying database. + pub async fn find_rollout_path_by_id( + &self, + id: ThreadId, + archived_only: Option, + ) -> anyhow::Result> { + let mut builder = + QueryBuilder::::new("SELECT rollout_path FROM threads WHERE id = "); + builder.push_bind(id.to_string()); + match archived_only { + Some(true) => { + builder.push(" AND archived = 1"); + } + Some(false) => { + builder.push(" AND archived = 0"); + } + None => {} + } + let row = builder.build().fetch_optional(self.pool.as_ref()).await?; + Ok(row + .and_then(|r| r.try_get::("rollout_path").ok()) + .map(PathBuf::from)) + } + + /// List threads using the underlying database. + #[allow(clippy::too_many_arguments)] + pub async fn list_threads( + &self, + page_size: usize, + anchor: Option<&crate::Anchor>, + sort_key: crate::SortKey, + allowed_sources: &[String], + model_providers: Option<&[String]>, + archived_only: bool, + search_term: Option<&str>, + ) -> anyhow::Result { + let limit = page_size.saturating_add(1); + + let mut builder = QueryBuilder::::new( + r#" +SELECT + id, + rollout_path, + created_at, + updated_at, + source, + agent_nickname, + agent_role, + model_provider, + cwd, + cli_version, + title, + sandbox_policy, + approval_mode, + tokens_used, + first_user_message, + archived_at, + git_sha, + git_branch, + git_origin_url +FROM threads + "#, + ); + push_thread_filters( + &mut builder, + archived_only, + allowed_sources, + model_providers, + anchor, + sort_key, + search_term, + ); + push_thread_order_and_limit(&mut builder, sort_key, limit); + + let rows = builder.build().fetch_all(self.pool.as_ref()).await?; + let mut items = rows + .into_iter() + .map(|row| ThreadRow::try_from_row(&row).and_then(ThreadMetadata::try_from)) + .collect::, _>>()?; + let num_scanned_rows = items.len(); + let next_anchor = if items.len() > page_size { + items.pop(); + items + .last() + .and_then(|item| anchor_from_item(item, sort_key)) + } else { + None + }; + Ok(ThreadsPage { + items, + next_anchor, + num_scanned_rows, + }) + } + + /// List thread ids using the underlying database (no rollout scanning). + pub async fn list_thread_ids( + &self, + limit: usize, + anchor: Option<&crate::Anchor>, + sort_key: crate::SortKey, + allowed_sources: &[String], + model_providers: Option<&[String]>, + archived_only: bool, + ) -> anyhow::Result> { + let mut builder = QueryBuilder::::new("SELECT id FROM threads"); + push_thread_filters( + &mut builder, + archived_only, + allowed_sources, + model_providers, + anchor, + sort_key, + None, + ); + push_thread_order_and_limit(&mut builder, sort_key, limit); + + let rows = builder.build().fetch_all(self.pool.as_ref()).await?; + rows.into_iter() + .map(|row| { + let id: String = row.try_get("id")?; + Ok(ThreadId::try_from(id)?) + }) + .collect() + } + + /// Insert or replace thread metadata directly. + pub async fn upsert_thread(&self, metadata: &crate::ThreadMetadata) -> anyhow::Result<()> { + self.upsert_thread_with_creation_memory_mode(metadata, None) + .await + } + + async fn upsert_thread_with_creation_memory_mode( + &self, + metadata: &crate::ThreadMetadata, + creation_memory_mode: Option<&str>, + ) -> anyhow::Result<()> { + sqlx::query( + r#" +INSERT INTO threads ( + id, + rollout_path, + created_at, + updated_at, + source, + agent_nickname, + agent_role, + model_provider, + cwd, + cli_version, + title, + sandbox_policy, + approval_mode, + tokens_used, + first_user_message, + archived, + archived_at, + git_sha, + git_branch, + git_origin_url, + memory_mode +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET + rollout_path = excluded.rollout_path, + created_at = excluded.created_at, + updated_at = excluded.updated_at, + source = excluded.source, + agent_nickname = excluded.agent_nickname, + agent_role = excluded.agent_role, + model_provider = excluded.model_provider, + cwd = excluded.cwd, + cli_version = excluded.cli_version, + title = excluded.title, + sandbox_policy = excluded.sandbox_policy, + approval_mode = excluded.approval_mode, + tokens_used = excluded.tokens_used, + first_user_message = excluded.first_user_message, + archived = excluded.archived, + archived_at = excluded.archived_at, + git_sha = excluded.git_sha, + git_branch = excluded.git_branch, + git_origin_url = excluded.git_origin_url + "#, + ) + .bind(metadata.id.to_string()) + .bind(metadata.rollout_path.display().to_string()) + .bind(datetime_to_epoch_seconds(metadata.created_at)) + .bind(datetime_to_epoch_seconds(metadata.updated_at)) + .bind(metadata.source.as_str()) + .bind(metadata.agent_nickname.as_deref()) + .bind(metadata.agent_role.as_deref()) + .bind(metadata.model_provider.as_str()) + .bind(metadata.cwd.display().to_string()) + .bind(metadata.cli_version.as_str()) + .bind(metadata.title.as_str()) + .bind(metadata.sandbox_policy.as_str()) + .bind(metadata.approval_mode.as_str()) + .bind(metadata.tokens_used) + .bind(metadata.first_user_message.as_deref().unwrap_or_default()) + .bind(metadata.archived_at.is_some()) + .bind(metadata.archived_at.map(datetime_to_epoch_seconds)) + .bind(metadata.git_sha.as_deref()) + .bind(metadata.git_branch.as_deref()) + .bind(metadata.git_origin_url.as_deref()) + .bind(creation_memory_mode.unwrap_or("enabled")) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + + /// Persist dynamic tools for a thread if none have been stored yet. + /// + /// Dynamic tools are defined at thread start and should not change afterward. + /// This only writes the first time we see tools for a given thread. + pub async fn persist_dynamic_tools( + &self, + thread_id: ThreadId, + tools: Option<&[DynamicToolSpec]>, + ) -> anyhow::Result<()> { + let Some(tools) = tools else { + return Ok(()); + }; + if tools.is_empty() { + return Ok(()); + } + let thread_id = thread_id.to_string(); + let mut tx = self.pool.begin().await?; + for (idx, tool) in tools.iter().enumerate() { + let position = i64::try_from(idx).unwrap_or(i64::MAX); + let input_schema = serde_json::to_string(&tool.input_schema)?; + sqlx::query( + r#" +INSERT INTO thread_dynamic_tools ( + thread_id, + position, + name, + description, + input_schema +) VALUES (?, ?, ?, ?, ?) +ON CONFLICT(thread_id, position) DO NOTHING + "#, + ) + .bind(thread_id.as_str()) + .bind(position) + .bind(tool.name.as_str()) + .bind(tool.description.as_str()) + .bind(input_schema) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + Ok(()) + } + + /// Apply rollout items incrementally using the underlying database. + pub async fn apply_rollout_items( + &self, + builder: &ThreadMetadataBuilder, + items: &[RolloutItem], + otel: Option<&OtelManager>, + new_thread_memory_mode: Option<&str>, + ) -> anyhow::Result<()> { + if items.is_empty() { + return Ok(()); + } + let existing_metadata = self.get_thread(builder.id).await?; + let mut metadata = existing_metadata + .clone() + .unwrap_or_else(|| builder.build(&self.default_provider)); + metadata.rollout_path = builder.rollout_path.clone(); + for item in items { + apply_rollout_item(&mut metadata, item, &self.default_provider); + } + if let Some(updated_at) = file_modified_time_utc(builder.rollout_path.as_path()).await { + metadata.updated_at = updated_at; + } + // Keep the thread upsert before dynamic tools to satisfy the foreign key constraint: + // thread_dynamic_tools.thread_id -> threads.id. + let upsert_result = if existing_metadata.is_none() { + self.upsert_thread_with_creation_memory_mode(&metadata, new_thread_memory_mode) + .await + } else { + self.upsert_thread(&metadata).await + }; + if let Err(err) = upsert_result { + if let Some(otel) = otel { + otel.counter(DB_ERROR_METRIC, 1, &[("stage", "apply_rollout_items")]); + } + return Err(err); + } + let dynamic_tools = extract_dynamic_tools(items); + if let Some(dynamic_tools) = dynamic_tools + && let Err(err) = self + .persist_dynamic_tools(builder.id, dynamic_tools.as_deref()) + .await + { + if let Some(otel) = otel { + otel.counter(DB_ERROR_METRIC, 1, &[("stage", "persist_dynamic_tools")]); + } + return Err(err); + } + Ok(()) + } + + /// Mark a thread as archived using the underlying database. + pub async fn mark_archived( + &self, + thread_id: ThreadId, + rollout_path: &Path, + archived_at: DateTime, + ) -> anyhow::Result<()> { + let Some(mut metadata) = self.get_thread(thread_id).await? else { + return Ok(()); + }; + metadata.archived_at = Some(archived_at); + metadata.rollout_path = rollout_path.to_path_buf(); + if let Some(updated_at) = file_modified_time_utc(rollout_path).await { + metadata.updated_at = updated_at; + } + if metadata.id != thread_id { + warn!( + "thread id mismatch during archive: expected {thread_id}, got {}", + metadata.id + ); + } + self.upsert_thread(&metadata).await + } + + /// Mark a thread as unarchived using the underlying database. + pub async fn mark_unarchived( + &self, + thread_id: ThreadId, + rollout_path: &Path, + ) -> anyhow::Result<()> { + let Some(mut metadata) = self.get_thread(thread_id).await? else { + return Ok(()); + }; + metadata.archived_at = None; + metadata.rollout_path = rollout_path.to_path_buf(); + if let Some(updated_at) = file_modified_time_utc(rollout_path).await { + metadata.updated_at = updated_at; + } + if metadata.id != thread_id { + warn!( + "thread id mismatch during unarchive: expected {thread_id}, got {}", + metadata.id + ); + } + self.upsert_thread(&metadata).await + } + + /// Delete a thread metadata row by id. + pub async fn delete_thread(&self, thread_id: ThreadId) -> anyhow::Result { + let result = sqlx::query("DELETE FROM threads WHERE id = ?") + .bind(thread_id.to_string()) + .execute(self.pool.as_ref()) + .await?; + Ok(result.rows_affected()) + } +} + +pub(super) fn extract_dynamic_tools(items: &[RolloutItem]) -> Option>> { + items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.dynamic_tools.clone()), + RolloutItem::ResponseItem(_) + | RolloutItem::Compacted(_) + | RolloutItem::TurnContext(_) + | RolloutItem::EventMsg(_) => None, + }) +} + +pub(super) fn push_thread_filters<'a>( + builder: &mut QueryBuilder<'a, Sqlite>, + archived_only: bool, + allowed_sources: &'a [String], + model_providers: Option<&'a [String]>, + anchor: Option<&crate::Anchor>, + sort_key: SortKey, + search_term: Option<&'a str>, +) { + builder.push(" WHERE 1 = 1"); + if archived_only { + builder.push(" AND archived = 1"); + } else { + builder.push(" AND archived = 0"); + } + builder.push(" AND first_user_message <> ''"); + if !allowed_sources.is_empty() { + builder.push(" AND source IN ("); + let mut separated = builder.separated(", "); + for source in allowed_sources { + separated.push_bind(source); + } + separated.push_unseparated(")"); + } + if let Some(model_providers) = model_providers + && !model_providers.is_empty() + { + builder.push(" AND model_provider IN ("); + let mut separated = builder.separated(", "); + for provider in model_providers { + separated.push_bind(provider); + } + separated.push_unseparated(")"); + } + if let Some(search_term) = search_term { + builder.push(" AND instr(title, "); + builder.push_bind(search_term); + builder.push(") > 0"); + } + if let Some(anchor) = anchor { + let anchor_ts = datetime_to_epoch_seconds(anchor.ts); + let column = match sort_key { + SortKey::CreatedAt => "created_at", + SortKey::UpdatedAt => "updated_at", + }; + builder.push(" AND ("); + builder.push(column); + builder.push(" < "); + builder.push_bind(anchor_ts); + builder.push(" OR ("); + builder.push(column); + builder.push(" = "); + builder.push_bind(anchor_ts); + builder.push(" AND id < "); + builder.push_bind(anchor.id.to_string()); + builder.push("))"); + } +} + +pub(super) fn push_thread_order_and_limit( + builder: &mut QueryBuilder<'_, Sqlite>, + sort_key: SortKey, + limit: usize, +) { + let order_column = match sort_key { + SortKey::CreatedAt => "created_at", + SortKey::UpdatedAt => "updated_at", + }; + builder.push(" ORDER BY "); + builder.push(order_column); + builder.push(" DESC, id DESC"); + builder.push(" LIMIT "); + builder.push_bind(limit as i64); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::test_support::test_thread_metadata; + use crate::runtime::test_support::unique_temp_dir; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn upsert_thread_keeps_creation_memory_mode_for_existing_rows() { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None) + .await + .expect("state db should initialize"); + let thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id"); + let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone()); + + runtime + .upsert_thread_with_creation_memory_mode(&metadata, Some("disabled")) + .await + .expect("initial insert should succeed"); + + let memory_mode: String = + sqlx::query_scalar("SELECT memory_mode FROM threads WHERE id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("memory mode should be readable"); + assert_eq!(memory_mode, "disabled"); + + metadata.title = "updated title".to_string(); + runtime + .upsert_thread(&metadata) + .await + .expect("upsert should succeed"); + + let memory_mode: String = + sqlx::query_scalar("SELECT memory_mode FROM threads WHERE id = ?") + .bind(thread_id.to_string()) + .fetch_one(runtime.pool.as_ref()) + .await + .expect("memory mode should remain readable"); + assert_eq!(memory_mode, "disabled"); + } +} diff --git a/codex-rs/test-macros/BUILD.bazel b/codex-rs/test-macros/BUILD.bazel new file mode 100644 index 0000000000..0ff074f5e5 --- /dev/null +++ b/codex-rs/test-macros/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "test-macros", + crate_name = "codex_test_macros", + proc_macro = True, +) diff --git a/codex-rs/test-macros/Cargo.toml b/codex-rs/test-macros/Cargo.toml new file mode 100644 index 0000000000..275580322d --- /dev/null +++ b/codex-rs/test-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "codex-test-macros" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } + +[lints] +workspace = true diff --git a/codex-rs/test-macros/src/lib.rs b/codex-rs/test-macros/src/lib.rs new file mode 100644 index 0000000000..16c87f8582 --- /dev/null +++ b/codex-rs/test-macros/src/lib.rs @@ -0,0 +1,155 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::Attribute; +use syn::ItemFn; +use syn::parse::Nothing; +use syn::parse_macro_input; +use syn::parse_quote; + +const LARGE_STACK_TEST_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024; + +/// Run a test body on a dedicated thread with a larger stack. +/// +/// For async tests, this macro creates a Tokio multi-thread runtime with two +/// worker threads and blocks on the original async body inside the large-stack +/// thread. +#[proc_macro_attribute] +pub fn large_stack_test(attr: TokenStream, item: TokenStream) -> TokenStream { + parse_macro_input!(attr as Nothing); + + let item = parse_macro_input!(item as ItemFn); + expand_large_stack_test(item).into() +} + +fn expand_large_stack_test(mut item: ItemFn) -> TokenStream2 { + let attrs = filtered_attributes(&item.attrs); + item.attrs = attrs; + + let is_async = item.sig.asyncness.take().is_some(); + let name = &item.sig.ident; + let body = &item.block; + + let thread_body = if is_async { + quote! { + { + let runtime = ::tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .unwrap_or_else(|error| { + panic!("failed to build tokio runtime for large-stack test: {error}") + }); + runtime.block_on(async move #body) + } + } + } else { + quote! { #body } + }; + + *item.block = parse_quote!({ + let handle = ::std::thread::Builder::new() + .name(::std::string::String::from(::std::stringify!(#name))) + .stack_size(#LARGE_STACK_TEST_STACK_SIZE_BYTES) + .spawn(move || #thread_body) + .unwrap_or_else(|error| { + panic!("failed to spawn large-stack test thread: {error}") + }); + + match handle.join() { + Ok(result) => result, + Err(payload) => ::std::panic::resume_unwind(payload), + } + }); + + quote! { #item } +} + +fn filtered_attributes(attrs: &[Attribute]) -> Vec { + let mut filtered = Vec::with_capacity(attrs.len() + 1); + let mut has_test_attr = false; + + for attr in attrs { + if is_tokio_test_attr(attr) { + continue; + } + if is_test_attr(attr) || is_test_case_attr(attr) { + has_test_attr = true; + } + filtered.push(attr.clone()); + } + + if !has_test_attr { + filtered.push(parse_quote!(#[test])); + } + + filtered +} + +fn is_test_attr(attr: &Attribute) -> bool { + attr.path().is_ident("test") +} + +fn is_test_case_attr(attr: &Attribute) -> bool { + attr.path().is_ident("test_case") +} + +fn is_tokio_test_attr(attr: &Attribute) -> bool { + let mut segments = attr.path().segments.iter(); + matches!( + (segments.next(), segments.next(), segments.next()), + (Some(first), Some(second), None) if first.ident == "tokio" && second.ident == "test" + ) +} + +#[cfg(test)] +mod tests { + use super::expand_large_stack_test; + use syn::ItemFn; + use syn::parse_quote; + + fn has_attr(item: &ItemFn, name: &str) -> bool { + item.attrs.iter().any(|attr| attr.path().is_ident(name)) + } + + #[test] + fn adds_test_attribute_when_missing() { + let item: ItemFn = parse_quote! { + fn sample() {} + }; + + let expanded_tokens = expand_large_stack_test(item); + let expanded: ItemFn = match syn::parse2(expanded_tokens) { + Ok(expanded) => expanded, + Err(error) => panic!("failed to parse expanded function: {error}"), + }; + + assert!(has_attr(&expanded, "test")); + let body = quote::quote!(#expanded).to_string(); + assert!(body.contains("stack_size")); + } + + #[test] + fn removes_tokio_test_and_keeps_test_case() { + let item: ItemFn = parse_quote! { + #[test_case(1)] + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn sample(value: usize) -> anyhow::Result<()> { + let _ = value; + Ok(()) + } + }; + + let expanded_tokens = expand_large_stack_test(item); + let expanded: ItemFn = match syn::parse2(expanded_tokens) { + Ok(expanded) => expanded, + Err(error) => panic!("failed to parse expanded function: {error}"), + }; + + assert!(has_attr(&expanded, "test_case")); + assert!(!has_attr(&expanded, "test")); + let body = quote::quote!(#expanded).to_string(); + assert!(body.contains("tokio :: runtime :: Builder")); + assert!(!body.contains("tokio :: test")); + } +} diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 1a87314e98..594acb33d4 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -50,6 +50,7 @@ codex-utils-fuzzy-match = { workspace = true } codex-utils-oss = { workspace = true } codex-utils-sandbox-summary = { workspace = true } codex-utils-sleep-inhibitor = { workspace = true } +codex-utils-string = { workspace = true } color-eyre = { workspace = true } crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] } derive_more = { workspace = true, features = ["is_variant"] } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ffcf4a3469..b3f322fd45 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1,6 +1,7 @@ use crate::app_backtrack::BacktrackState; use crate::app_event::AppEvent; use crate::app_event::ExitMode; +use crate::app_event::RealtimeAudioDeviceKind; #[cfg(target_os = "windows")] use crate::app_event::WindowsSandboxEnableMode; use crate::app_event_sender::AppEventSender; @@ -48,6 +49,7 @@ use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; @@ -350,6 +352,15 @@ impl ThreadEventStore { fn op_can_change_pending_replay_state(op: &Op) -> bool { PendingInteractiveReplayState::op_can_change_state(op) } + + fn event_can_change_pending_thread_approvals(event: &Event) -> bool { + PendingInteractiveReplayState::event_can_change_pending_thread_approvals(event) + } + + fn has_pending_thread_approvals(&self) -> bool { + self.pending_interactive_replay + .has_pending_thread_approvals() + } } #[derive(Debug)] @@ -850,6 +861,7 @@ impl App { }; self.active_thread_id = Some(thread_id); self.active_thread_rx = receiver; + self.refresh_pending_thread_approvals().await; } async fn store_active_thread_receiver(&mut self) { @@ -883,6 +895,7 @@ impl App { self.set_thread_active(active_id, false).await; } self.active_thread_rx = None; + self.refresh_pending_thread_approvals().await; } async fn note_active_thread_outbound_op(&mut self, op: &Op) { @@ -899,7 +912,63 @@ impl App { store.note_outbound_op(op); } + async fn refresh_pending_thread_approvals(&mut self) { + let channels: Vec<(ThreadId, Arc>)> = self + .thread_event_channels + .iter() + .map(|(thread_id, channel)| (*thread_id, Arc::clone(&channel.store))) + .collect(); + + let mut pending_thread_ids = Vec::new(); + for (thread_id, store) in channels { + if Some(thread_id) == self.active_thread_id { + continue; + } + + let store = store.lock().await; + if store.has_pending_thread_approvals() { + pending_thread_ids.push(thread_id); + } + } + + pending_thread_ids.sort_by_key(ThreadId::to_string); + + let threads = pending_thread_ids + .into_iter() + .map(|thread_id| { + let is_primary = self.primary_thread_id == Some(thread_id); + let fallback_label = if is_primary { + "Main [default]".to_string() + } else { + let thread_id = thread_id.to_string(); + let short_id: String = thread_id.chars().take(8).collect(); + format!("Agent ({short_id})") + }; + if let Some(entry) = self.agent_picker_threads.get(&thread_id) { + let label = format_agent_picker_item_name( + entry.agent_nickname.as_deref(), + entry.agent_role.as_deref(), + is_primary, + ); + if label == "Agent" { + let thread_id = thread_id.to_string(); + let short_id: String = thread_id.chars().take(8).collect(); + format!("{label} ({short_id})") + } else { + label + } + } else { + fallback_label + } + }) + .collect(); + + self.chat_widget.set_pending_thread_approvals(threads); + } + async fn enqueue_thread_event(&mut self, thread_id: ThreadId, event: Event) -> Result<()> { + let refresh_pending_thread_approvals = + ThreadEventStore::event_can_change_pending_thread_approvals(&event); let (sender, store) = { let channel = self.ensure_thread_channel(thread_id); (channel.sender.clone(), Arc::clone(&channel.store)) @@ -929,6 +998,9 @@ impl App { } } } + if refresh_pending_thread_approvals { + self.refresh_pending_thread_approvals().await; + } Ok(()) } @@ -1107,6 +1179,7 @@ impl App { ); } self.drain_active_thread_events(tui).await?; + self.refresh_pending_thread_approvals().await; Ok(()) } @@ -1130,6 +1203,7 @@ impl App { self.active_thread_rx = None; self.primary_thread_id = None; self.pending_primary_events.clear(); + self.chat_widget.set_pending_thread_approvals(Vec::new()); } async fn start_fresh_session_with_summary_hint(&mut self, tui: &mut tui::Tui) { @@ -1287,6 +1361,11 @@ impl App { auth_manager.clone(), SessionSource::Cli, config.model_catalog.clone(), + CollaborationModesConfig { + default_mode_request_user_input: config + .features + .enabled(codex_core::features::Feature::DefaultModeRequestUserInput), + }, )); let mut model = thread_manager .get_models_manager() @@ -1376,12 +1455,16 @@ impl App { }; ChatWidget::new(init, thread_manager.clone()) } - SessionSelection::Resume(path) => { + SessionSelection::Resume(target_session) => { let resumed = thread_manager - .resume_thread_from_rollout(config.clone(), path.clone(), auth_manager.clone()) + .resume_thread_from_rollout( + config.clone(), + target_session.path.clone(), + auth_manager.clone(), + ) .await .wrap_err_with(|| { - let path_display = path.display(); + let path_display = target_session.path.display(); format!("Failed to resume session from {path_display}") })?; let init = crate::chatwidget::ChatWidgetInit { @@ -1406,13 +1489,18 @@ impl App { }; ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured) } - SessionSelection::Fork(path) => { + SessionSelection::Fork(target_session) => { otel_manager.counter("codex.thread.fork", 1, &[("source", "cli_subcommand")]); let forked = thread_manager - .fork_thread(usize::MAX, config.clone(), path.clone(), false) + .fork_thread( + usize::MAX, + config.clone(), + target_session.path.clone(), + false, + ) .await .wrap_err_with(|| { - let path_display = path.display(); + let path_display = target_session.path.display(); format!("Failed to fork session from {path_display}") })?; let init = crate::chatwidget::ChatWidgetInit { @@ -1680,12 +1768,14 @@ impl App { } AppEvent::OpenResumePicker => { match crate::resume_picker::run_resume_picker(tui, &self.config, false).await? { - SessionSelection::Resume(path) => { + SessionSelection::Resume(target_session) => { let current_cwd = self.config.cwd.clone(); let resume_cwd = match crate::resolve_cwd_for_resume_or_fork( tui, + &self.config, ¤t_cwd, - &path, + target_session.thread_id, + &target_session.path, CwdPromptAction::Resume, true, ) @@ -1721,7 +1811,7 @@ impl App { .server .resume_thread_from_rollout( resume_config.clone(), - path.clone(), + target_session.path.clone(), self.auth_manager.clone(), ) .await @@ -1755,7 +1845,7 @@ impl App { } } Err(err) => { - let path_display = path.display(); + let path_display = target_session.path.display(); self.chat_widget.add_error_message(format!( "Failed to resume session from {path_display}: {err}" )); @@ -1870,19 +1960,9 @@ impl App { AppEvent::CodexEvent(event) => { self.enqueue_primary_event(event).await?; } - AppEvent::Exit(mode) => match mode { - ExitMode::ShutdownFirst => { - // Mark the thread we are explicitly shutting down for exit so - // its shutdown completion does not trigger agent failover. - self.pending_shutdown_exit_thread_id = - self.active_thread_id.or(self.chat_widget.thread_id()); - self.chat_widget.submit_op(Op::Shutdown); - } - ExitMode::Immediate => { - self.pending_shutdown_exit_thread_id = None; - return Ok(AppRunControl::Exit(ExitReason::UserRequested)); - } - }, + AppEvent::Exit(mode) => { + return Ok(self.handle_exit_mode(mode)); + } AppEvent::FatalExitRequest(message) => { return Ok(AppRunControl::Exit(ExitReason::Fatal(message))); } @@ -1892,6 +1972,7 @@ impl App { let submitted = self.chat_widget.submit_op(op); if submitted && let Some(op) = replay_state_op.as_ref() { self.note_active_thread_outbound_op(op).await; + self.refresh_pending_thread_approvals().await; } } AppEvent::DiffResult(text) => { @@ -1963,6 +2044,9 @@ impl App { AppEvent::UpdatePersonality(personality) => { self.on_update_personality(personality); } + AppEvent::OpenRealtimeAudioDeviceSelection { kind } => { + self.chat_widget.open_realtime_audio_device_selection(kind); + } AppEvent::OpenReasoningPopup { model } => { self.chat_widget.open_reasoning_popup(model); } @@ -2388,6 +2472,56 @@ impl App { } } } + AppEvent::PersistRealtimeAudioDeviceSelection { kind, name } => { + let builder = match kind { + RealtimeAudioDeviceKind::Microphone => { + ConfigEditsBuilder::new(&self.config.codex_home) + .set_realtime_microphone(name.as_deref()) + } + RealtimeAudioDeviceKind::Speaker => { + ConfigEditsBuilder::new(&self.config.codex_home) + .set_realtime_speaker(name.as_deref()) + } + }; + + match builder.apply().await { + Ok(()) => { + match kind { + RealtimeAudioDeviceKind::Microphone => { + self.config.realtime_audio.microphone = name.clone(); + } + RealtimeAudioDeviceKind::Speaker => { + self.config.realtime_audio.speaker = name.clone(); + } + } + self.chat_widget + .set_realtime_audio_device(kind, name.clone()); + + if self.chat_widget.realtime_conversation_is_live() { + self.chat_widget.open_realtime_audio_restart_prompt(kind); + } else { + let selection = name.unwrap_or_else(|| "System default".to_string()); + self.chat_widget.add_info_message( + format!("Realtime {} set to {selection}", kind.noun()), + None, + ); + } + } + Err(err) => { + tracing::error!( + error = %err, + "failed to persist realtime audio selection" + ); + self.chat_widget.add_error_message(format!( + "Failed to save realtime {}: {err}", + kind.noun() + )); + } + } + } + AppEvent::RestartRealtimeAudioDevice { kind } => { + self.chat_widget.restart_realtime_audio_device(kind); + } AppEvent::UpdateAskForApprovalPolicy(policy) => { self.runtime_approval_policy_override = Some(policy); if let Err(err) = self.config.permissions.approval_policy.set(policy) { @@ -2638,6 +2772,9 @@ impl App { AppEvent::OpenAgentPicker => { self.open_agent_picker().await; } + AppEvent::RefreshPendingThreadApprovals => { + self.refresh_pending_thread_approvals().await; + } AppEvent::SelectAgentThread(thread_id) => { self.select_agent_thread(tui, thread_id).await?; } @@ -2855,6 +2992,27 @@ impl App { Ok(AppRunControl::Continue) } + fn handle_exit_mode(&mut self, mode: ExitMode) -> AppRunControl { + match mode { + ExitMode::ShutdownFirst => { + // Mark the thread we are explicitly shutting down for exit so + // its shutdown completion does not trigger agent failover. + self.pending_shutdown_exit_thread_id = + self.active_thread_id.or(self.chat_widget.thread_id()); + if self.chat_widget.submit_op(Op::Shutdown) { + AppRunControl::Continue + } else { + self.pending_shutdown_exit_thread_id = None; + AppRunControl::Exit(ExitReason::UserRequested) + } + } + ExitMode::Immediate => { + self.pending_shutdown_exit_thread_id = None; + AppRunControl::Exit(ExitReason::UserRequested) + } + } + } + fn handle_codex_event_now(&mut self, event: Event) { let needs_refresh = matches!( event.msg, @@ -3026,6 +3184,7 @@ impl App { ThreadEventChannel::new_with_session_configured(THREAD_EVENT_CHANNEL_CAPACITY, event); let sender = channel.sender.clone(); let store = Arc::clone(&channel.store); + let app_event_tx = self.app_event_tx.clone(); self.thread_event_channels.insert(thread_id, channel); tokio::spawn(async move { loop { @@ -3036,11 +3195,16 @@ impl App { break; } }; + let refresh_pending_thread_approvals = + ThreadEventStore::event_can_change_pending_thread_approvals(&event); let should_send = { let mut guard = store.lock().await; guard.push_event(event.clone()); guard.active }; + if refresh_pending_thread_approvals { + app_event_tx.send(AppEvent::RefreshPendingThreadApprovals); + } if should_send && let Err(err) = sender.send(event).await { tracing::debug!("external thread {thread_id} channel closed: {err}"); break; @@ -3373,15 +3537,21 @@ mod tests { true ); assert_eq!( - App::should_wait_for_initial_session(&SessionSelection::Resume(PathBuf::from( - "/tmp/restore" - ))), + App::should_wait_for_initial_session(&SessionSelection::Resume( + crate::resume_picker::SessionTarget { + path: PathBuf::from("/tmp/restore"), + thread_id: ThreadId::new(), + } + )), false ); assert_eq!( - App::should_wait_for_initial_session(&SessionSelection::Fork(PathBuf::from( - "/tmp/fork" - ))), + App::should_wait_for_initial_session(&SessionSelection::Fork( + crate::resume_picker::SessionTarget { + path: PathBuf::from("/tmp/fork"), + thread_id: ThreadId::new(), + } + )), false ); } @@ -3417,14 +3587,20 @@ mod tests { #[test] fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() { let wait_for_resume = App::should_wait_for_initial_session(&SessionSelection::Resume( - PathBuf::from("/tmp/restore"), + crate::resume_picker::SessionTarget { + path: PathBuf::from("/tmp/restore"), + thread_id: ThreadId::new(), + }, )); assert_eq!( App::should_handle_active_thread_events(wait_for_resume, true), true ); let wait_for_fork = App::should_wait_for_initial_session(&SessionSelection::Fork( - PathBuf::from("/tmp/fork"), + crate::resume_picker::SessionTarget { + path: PathBuf::from("/tmp/fork"), + thread_id: ThreadId::new(), + }, )); assert_eq!( App::should_handle_active_thread_events(wait_for_fork, true), @@ -3523,6 +3699,64 @@ mod tests { Ok(()) } + #[tokio::test] + async fn refresh_pending_thread_approvals_only_lists_inactive_threads() { + let mut app = make_test_app().await; + let main_thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000001").expect("valid thread"); + let agent_thread_id = + ThreadId::from_string("00000000-0000-0000-0000-000000000002").expect("valid thread"); + + app.primary_thread_id = Some(main_thread_id); + app.active_thread_id = Some(main_thread_id); + app.thread_event_channels + .insert(main_thread_id, ThreadEventChannel::new(1)); + + let agent_channel = ThreadEventChannel::new(1); + { + let mut store = agent_channel.store.lock().await; + store.push_event(Event { + id: "ev-1".to_string(), + msg: EventMsg::ExecApprovalRequest( + codex_protocol::protocol::ExecApprovalRequestEvent { + call_id: "call-1".to_string(), + approval_id: None, + turn_id: "turn-1".to_string(), + command: vec!["echo".to_string(), "hi".to_string()], + cwd: PathBuf::from("/tmp"), + reason: None, + network_approval_context: None, + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + additional_permissions: None, + available_decisions: None, + parsed_cmd: Vec::new(), + }, + ), + }); + } + app.thread_event_channels + .insert(agent_thread_id, agent_channel); + app.agent_picker_threads.insert( + agent_thread_id, + AgentPickerThreadEntry { + agent_nickname: Some("Robie".to_string()), + agent_role: Some("explorer".to_string()), + is_closed: false, + }, + ); + + app.refresh_pending_thread_approvals().await; + assert_eq!( + app.chat_widget.pending_thread_approvals(), + &["Robie [explorer]".to_string()] + ); + + app.active_thread_id = Some(agent_thread_id); + app.refresh_pending_thread_approvals().await; + assert!(app.chat_widget.pending_thread_approvals().is_empty()); + } + #[test] fn agent_picker_item_name_snapshot() { let thread_id = @@ -4627,6 +4861,34 @@ mod tests { } } + #[tokio::test] + async fn shutdown_first_exit_returns_immediate_exit_when_shutdown_submit_fails() { + let mut app = make_test_app().await; + let thread_id = ThreadId::new(); + app.active_thread_id = Some(thread_id); + + let control = app.handle_exit_mode(ExitMode::ShutdownFirst); + + assert_eq!(app.pending_shutdown_exit_thread_id, None); + assert!(matches!( + control, + AppRunControl::Exit(ExitReason::UserRequested) + )); + } + + #[tokio::test] + async fn shutdown_first_exit_waits_for_shutdown_when_submit_succeeds() { + let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; + let thread_id = ThreadId::new(); + app.active_thread_id = Some(thread_id); + + let control = app.handle_exit_mode(ExitMode::ShutdownFirst); + + assert_eq!(app.pending_shutdown_exit_thread_id, Some(thread_id)); + assert!(matches!(control, AppRunControl::Continue)); + assert_eq!(op_rx.try_recv(), Ok(Op::Shutdown)); + } + #[tokio::test] async fn clear_only_ui_reset_preserves_chat_session_state() { let mut app = make_test_app().await; diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index bc7fed6443..d82d014827 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -44,6 +44,20 @@ pub(super) struct PendingInteractiveReplayState { } impl PendingInteractiveReplayState { + pub(super) fn event_can_change_pending_thread_approvals(event: &Event) -> bool { + matches!( + &event.msg, + EventMsg::ExecApprovalRequest(_) + | EventMsg::ApplyPatchApprovalRequest(_) + | EventMsg::ElicitationRequest(_) + | EventMsg::ExecCommandBegin(_) + | EventMsg::PatchApplyBegin(_) + | EventMsg::TurnComplete(_) + | EventMsg::TurnAborted(_) + | EventMsg::ShutdownComplete + ) + } + pub(super) fn op_can_change_state(op: &Op) -> bool { matches!( op, @@ -111,11 +125,12 @@ impl PendingInteractiveReplayState { pub(super) fn note_event(&mut self, event: &Event) { match &event.msg { EventMsg::ExecApprovalRequest(ev) => { - self.exec_approval_call_ids.insert(ev.call_id.clone()); + let approval_id = ev.effective_approval_id(); + self.exec_approval_call_ids.insert(approval_id.clone()); self.exec_approval_call_ids_by_turn_id .entry(ev.turn_id.clone()) .or_default() - .push(ev.call_id.clone()); + .push(approval_id); } EventMsg::ExecCommandBegin(ev) => { self.exec_approval_call_ids.remove(&ev.call_id); @@ -173,11 +188,12 @@ impl PendingInteractiveReplayState { pub(super) fn note_evicted_event(&mut self, event: &Event) { match &event.msg { EventMsg::ExecApprovalRequest(ev) => { - self.exec_approval_call_ids.remove(&ev.call_id); + let approval_id = ev.effective_approval_id(); + self.exec_approval_call_ids.remove(&approval_id); Self::remove_call_id_from_turn_map_entry( &mut self.exec_approval_call_ids_by_turn_id, &ev.turn_id, - &ev.call_id, + &approval_id, ); } EventMsg::ApplyPatchApprovalRequest(ev) => { @@ -218,7 +234,9 @@ impl PendingInteractiveReplayState { pub(super) fn should_replay_snapshot_event(&self, event: &Event) -> bool { match &event.msg { - EventMsg::ExecApprovalRequest(ev) => self.exec_approval_call_ids.contains(&ev.call_id), + EventMsg::ExecApprovalRequest(ev) => self + .exec_approval_call_ids + .contains(&ev.effective_approval_id()), EventMsg::ApplyPatchApprovalRequest(ev) => { self.patch_approval_call_ids.contains(&ev.call_id) } @@ -236,6 +254,12 @@ impl PendingInteractiveReplayState { } } + pub(super) fn has_pending_thread_approvals(&self) -> bool { + !self.exec_approval_call_ids.is_empty() + || !self.patch_approval_call_ids.is_empty() + || !self.elicitation_requests.is_empty() + } + fn clear_request_user_input_turn(&mut self, turn_id: &str) { if let Some(call_ids) = self.request_user_input_call_ids_by_turn_id.remove(turn_id) { for call_id in call_ids { @@ -362,7 +386,7 @@ mod tests { } #[test] - fn thread_event_snapshot_drops_resolved_exec_approval_after_outbound_approval_call_id() { + fn thread_event_snapshot_drops_resolved_exec_approval_after_outbound_approval_id() { let mut store = ThreadEventStore::new(8); store.push_event(Event { id: "ev-1".to_string(), @@ -378,13 +402,14 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: Vec::new(), }, ), }); store.note_outbound_op(&Op::ExecApproval { - id: "call-1".to_string(), + id: "approval-1".to_string(), turn_id: Some("turn-1".to_string()), decision: codex_protocol::protocol::ReviewDecision::Approved, }); @@ -520,6 +545,7 @@ mod tests { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: Vec::new(), }, ), @@ -578,4 +604,57 @@ mod tests { "resolved elicitation prompt should not replay on thread switch" ); } + + #[test] + fn thread_event_store_reports_pending_thread_approvals() { + let mut store = ThreadEventStore::new(8); + assert_eq!(store.has_pending_thread_approvals(), false); + + store.push_event(Event { + id: "ev-1".to_string(), + msg: EventMsg::ExecApprovalRequest( + codex_protocol::protocol::ExecApprovalRequestEvent { + call_id: "call-1".to_string(), + approval_id: None, + turn_id: "turn-1".to_string(), + command: vec!["echo".to_string(), "hi".to_string()], + cwd: PathBuf::from("/tmp"), + reason: None, + network_approval_context: None, + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + additional_permissions: None, + available_decisions: None, + parsed_cmd: Vec::new(), + }, + ), + }); + + assert_eq!(store.has_pending_thread_approvals(), true); + + store.note_outbound_op(&Op::ExecApproval { + id: "call-1".to_string(), + turn_id: Some("turn-1".to_string()), + decision: codex_protocol::protocol::ReviewDecision::Approved, + }); + + assert_eq!(store.has_pending_thread_approvals(), false); + } + + #[test] + fn request_user_input_does_not_count_as_pending_thread_approval() { + let mut store = ThreadEventStore::new(8); + store.push_event(Event { + id: "ev-1".to_string(), + msg: EventMsg::RequestUserInput( + codex_protocol::request_user_input::RequestUserInputEvent { + call_id: "call-1".to_string(), + turn_id: "turn-1".to_string(), + questions: Vec::new(), + }, + ), + }); + + assert_eq!(store.has_pending_thread_approvals(), false); + } } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 27ebe8cfe3..104895ad9e 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -29,6 +29,28 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RealtimeAudioDeviceKind { + Microphone, + Speaker, +} + +impl RealtimeAudioDeviceKind { + pub(crate) fn title(self) -> &'static str { + match self { + Self::Microphone => "Microphone", + Self::Speaker => "Speaker", + } + } + + pub(crate) fn noun(self) -> &'static str { + match self { + Self::Microphone => "microphone", + Self::Speaker => "speaker", + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) enum WindowsSandboxEnableMode { @@ -51,6 +73,9 @@ pub(crate) enum AppEvent { /// Switch the active thread to the selected agent. SelectAgentThread(ThreadId), + /// Recompute the list of inactive threads that still need approval. + RefreshPendingThreadApprovals, + /// Start a new session. NewSession, @@ -163,6 +188,26 @@ pub(crate) enum AppEvent { personality: Personality, }, + /// Open the device picker for a realtime microphone or speaker. + OpenRealtimeAudioDeviceSelection { + kind: RealtimeAudioDeviceKind, + }, + + /// Persist the selected realtime microphone or speaker to top-level config. + #[cfg_attr( + any(target_os = "linux", not(feature = "voice-input")), + allow(dead_code) + )] + PersistRealtimeAudioDeviceSelection { + kind: RealtimeAudioDeviceKind, + name: Option, + }, + + /// Restart the selected realtime microphone or speaker locally. + RestartRealtimeAudioDevice { + kind: RealtimeAudioDeviceKind, + }, + /// Open the reasoning selection popup after picking a model. OpenReasoningPopup { model: ModelPreset, diff --git a/codex-rs/tui/src/audio_device.rs b/codex-rs/tui/src/audio_device.rs new file mode 100644 index 0000000000..6c3b22ccdd --- /dev/null +++ b/codex-rs/tui/src/audio_device.rs @@ -0,0 +1,176 @@ +use codex_core::config::Config; +use cpal::traits::DeviceTrait; +use cpal::traits::HostTrait; +use tracing::warn; + +use crate::app_event::RealtimeAudioDeviceKind; + +const PREFERRED_INPUT_SAMPLE_RATE: u32 = 24_000; +const PREFERRED_INPUT_CHANNELS: u16 = 1; + +pub(crate) fn list_realtime_audio_device_names( + kind: RealtimeAudioDeviceKind, +) -> Result, String> { + let host = cpal::default_host(); + let mut device_names = Vec::new(); + for device in devices(&host, kind)? { + let Ok(name) = device.name() else { + continue; + }; + if !device_names.contains(&name) { + device_names.push(name); + } + } + Ok(device_names) +} + +pub(crate) fn select_configured_input_device_and_config( + config: &Config, +) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> { + select_device_and_config(RealtimeAudioDeviceKind::Microphone, config) +} + +pub(crate) fn select_configured_output_device_and_config( + config: &Config, +) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> { + select_device_and_config(RealtimeAudioDeviceKind::Speaker, config) +} + +pub(crate) fn preferred_input_config( + device: &cpal::Device, +) -> Result { + let supported_configs = device + .supported_input_configs() + .map_err(|err| format!("failed to enumerate input audio configs: {err}"))?; + + supported_configs + .filter_map(|range| { + let sample_format_rank = match range.sample_format() { + cpal::SampleFormat::I16 => 0u8, + cpal::SampleFormat::U16 => 1u8, + cpal::SampleFormat::F32 => 2u8, + _ => return None, + }; + let sample_rate = preferred_input_sample_rate(&range); + let sample_rate_penalty = sample_rate.0.abs_diff(PREFERRED_INPUT_SAMPLE_RATE); + let channel_penalty = range.channels().abs_diff(PREFERRED_INPUT_CHANNELS); + Some(( + (sample_rate_penalty, channel_penalty, sample_format_rank), + range.with_sample_rate(sample_rate), + )) + }) + .min_by_key(|(score, _)| *score) + .map(|(_, config)| config) + .or_else(|| device.default_input_config().ok()) + .ok_or_else(|| "failed to get default input config".to_string()) +} + +fn select_device_and_config( + kind: RealtimeAudioDeviceKind, + config: &Config, +) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> { + let host = cpal::default_host(); + let configured_name = configured_name(kind, config); + let selected = configured_name + .and_then(|name| find_device_by_name(&host, kind, name)) + .or_else(|| { + let default_device = default_device(&host, kind); + if let Some(name) = configured_name && default_device.is_some() { + warn!( + "configured {} audio device `{name}` was unavailable; falling back to system default", + kind.noun() + ); + } + default_device + }) + .ok_or_else(|| missing_device_error(kind, configured_name))?; + + let stream_config = match kind { + RealtimeAudioDeviceKind::Microphone => preferred_input_config(&selected)?, + RealtimeAudioDeviceKind::Speaker => default_config(&selected, kind)?, + }; + Ok((selected, stream_config)) +} + +fn configured_name(kind: RealtimeAudioDeviceKind, config: &Config) -> Option<&str> { + match kind { + RealtimeAudioDeviceKind::Microphone => config.realtime_audio.microphone.as_deref(), + RealtimeAudioDeviceKind::Speaker => config.realtime_audio.speaker.as_deref(), + } +} + +fn find_device_by_name( + host: &cpal::Host, + kind: RealtimeAudioDeviceKind, + name: &str, +) -> Option { + let devices = devices(host, kind).ok()?; + devices + .into_iter() + .find(|device| device.name().ok().as_deref() == Some(name)) +} + +fn devices(host: &cpal::Host, kind: RealtimeAudioDeviceKind) -> Result, String> { + match kind { + RealtimeAudioDeviceKind::Microphone => host + .input_devices() + .map(|devices| devices.collect()) + .map_err(|err| format!("failed to enumerate input audio devices: {err}")), + RealtimeAudioDeviceKind::Speaker => host + .output_devices() + .map(|devices| devices.collect()) + .map_err(|err| format!("failed to enumerate output audio devices: {err}")), + } +} + +fn default_device(host: &cpal::Host, kind: RealtimeAudioDeviceKind) -> Option { + match kind { + RealtimeAudioDeviceKind::Microphone => host.default_input_device(), + RealtimeAudioDeviceKind::Speaker => host.default_output_device(), + } +} + +fn default_config( + device: &cpal::Device, + kind: RealtimeAudioDeviceKind, +) -> Result { + match kind { + RealtimeAudioDeviceKind::Microphone => device + .default_input_config() + .map_err(|err| format!("failed to get default input config: {err}")), + RealtimeAudioDeviceKind::Speaker => device + .default_output_config() + .map_err(|err| format!("failed to get default output config: {err}")), + } +} + +fn preferred_input_sample_rate(range: &cpal::SupportedStreamConfigRange) -> cpal::SampleRate { + let min = range.min_sample_rate().0; + let max = range.max_sample_rate().0; + if (min..=max).contains(&PREFERRED_INPUT_SAMPLE_RATE) { + cpal::SampleRate(PREFERRED_INPUT_SAMPLE_RATE) + } else if PREFERRED_INPUT_SAMPLE_RATE < min { + cpal::SampleRate(min) + } else { + cpal::SampleRate(max) + } +} + +fn missing_device_error(kind: RealtimeAudioDeviceKind, configured_name: Option<&str>) -> String { + match (kind, configured_name) { + (RealtimeAudioDeviceKind::Microphone, Some(name)) => { + format!( + "configured microphone `{name}` was unavailable and no default input audio device was found" + ) + } + (RealtimeAudioDeviceKind::Speaker, Some(name)) => { + format!( + "configured speaker `{name}` was unavailable and no default output audio device was found" + ) + } + (RealtimeAudioDeviceKind::Microphone, None) => { + "no input audio device available".to_string() + } + (RealtimeAudioDeviceKind::Speaker, None) => "no output audio device available".to_string(), + } +} diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 76b1c3d60d..b4a2de333f 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -18,11 +18,11 @@ use crate::render::renderable::ColumnRenderable; use crate::render::renderable::Renderable; use codex_core::features::Features; use codex_protocol::mcp::RequestId; -use codex_protocol::models::AdditionalPermissions; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ElicitationAction; -use codex_protocol::protocol::ExecPolicyAmendment; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::NetworkApprovalContext; +use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use crossterm::event::KeyCode; @@ -44,9 +44,9 @@ pub(crate) enum ApprovalRequest { id: String, command: Vec, reason: Option, + available_decisions: Vec, network_approval_context: Option, - proposed_execpolicy_amendment: Option, - additional_permissions: Option, + additional_permissions: Option, }, ApplyPatch { id: String, @@ -64,7 +64,6 @@ pub(crate) enum ApprovalRequest { /// Modal overlay asking the user to approve or deny one or more requests. pub(crate) struct ApprovalOverlay { current_request: Option, - current_variant: Option, queue: Vec, app_event_tx: AppEventSender, list: ListSelectionView, @@ -78,7 +77,6 @@ impl ApprovalOverlay { pub fn new(request: ApprovalRequest, app_event_tx: AppEventSender, features: Features) -> Self { let mut view = Self { current_request: None, - current_variant: None, queue: Vec::new(), app_event_tx: app_event_tx.clone(), list: ListSelectionView::new(Default::default(), app_event_tx), @@ -96,29 +94,28 @@ impl ApprovalOverlay { } fn set_current(&mut self, request: ApprovalRequest) { - self.current_request = Some(request.clone()); - let ApprovalRequestState { variant, header } = ApprovalRequestState::from(request); - self.current_variant = Some(variant.clone()); self.current_complete = false; - let (options, params) = Self::build_options(variant, header, &self.features); + let header = build_header(&request); + let (options, params) = Self::build_options(&request, header, &self.features); + self.current_request = Some(request); self.options = options; self.list = ListSelectionView::new(params, self.app_event_tx.clone()); } fn build_options( - variant: ApprovalVariant, + request: &ApprovalRequest, header: Box, _features: &Features, ) -> (Vec, SelectionViewParams) { - let (options, title) = match &variant { - ApprovalVariant::Exec { + let (options, title) = match request { + ApprovalRequest::Exec { + available_decisions, network_approval_context, - proposed_execpolicy_amendment, additional_permissions, .. } => ( exec_options( - proposed_execpolicy_amendment.clone(), + available_decisions, network_approval_context.as_ref(), additional_permissions.as_ref(), ), @@ -132,11 +129,11 @@ impl ApprovalOverlay { }, ), ), - ApprovalVariant::ApplyPatch { .. } => ( + ApprovalRequest::ApplyPatch { .. } => ( patch_options(), "Would you like to make the following edits?".to_string(), ), - ApprovalVariant::McpElicitation { server_name, .. } => ( + ApprovalRequest::McpElicitation { server_name, .. } => ( elicitation_options(), format!("{server_name} needs your approval."), ), @@ -183,18 +180,19 @@ impl ApprovalOverlay { let Some(option) = self.options.get(actual_idx) else { return; }; - if let Some(variant) = self.current_variant.as_ref() { - match (variant, &option.decision) { - (ApprovalVariant::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => { + if let Some(request) = self.current_request.as_ref() { + match (request, &option.decision) { + (ApprovalRequest::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => { self.handle_exec_decision(id, command, decision.clone()); } - (ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => { + (ApprovalRequest::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => { self.handle_patch_decision(id, decision.clone()); } ( - ApprovalVariant::McpElicitation { + ApprovalRequest::McpElicitation { server_name, request_id, + .. }, ApprovalDecision::McpElicitation(decision), ) => { @@ -295,18 +293,19 @@ impl BottomPaneView for ApprovalOverlay { return CancellationEvent::Handled; } if !self.current_complete - && let Some(variant) = self.current_variant.as_ref() + && let Some(request) = self.current_request.as_ref() { - match &variant { - ApprovalVariant::Exec { id, command, .. } => { + match request { + ApprovalRequest::Exec { id, command, .. } => { self.handle_exec_decision(id, command, ReviewDecision::Abort); } - ApprovalVariant::ApplyPatch { id, .. } => { + ApprovalRequest::ApplyPatch { id, .. } => { self.handle_patch_decision(id, ReviewDecision::Abort); } - ApprovalVariant::McpElicitation { + ApprovalRequest::McpElicitation { server_name, request_id, + .. } => { self.handle_elicitation_decision( server_name, @@ -348,119 +347,77 @@ impl Renderable for ApprovalOverlay { } } -struct ApprovalRequestState { - variant: ApprovalVariant, - header: Box, -} - -impl From for ApprovalRequestState { - fn from(value: ApprovalRequest) -> Self { - match value { - ApprovalRequest::Exec { - id, - command, - reason, - network_approval_context, - proposed_execpolicy_amendment, - additional_permissions, - } => { - let mut header: Vec> = Vec::new(); - if let Some(reason) = reason { - header.push(Line::from(vec!["Reason: ".into(), reason.italic()])); - header.push(Line::from("")); - } - if let Some(ref additional_permissions) = additional_permissions - && let Some(rule_line) = - format_additional_permissions_rule(additional_permissions) - { - header.push(Line::from(vec![ - "Permission rule: ".into(), - rule_line.cyan(), - ])); - header.push(Line::from("")); - } - let full_cmd = strip_bash_lc_and_escape(&command); - let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd); - if let Some(first) = full_cmd_lines.first_mut() { - first.spans.insert(0, Span::from("$ ")); - } - if network_approval_context.is_none() { - header.extend(full_cmd_lines); - } - Self { - variant: ApprovalVariant::Exec { - id, - command, - network_approval_context, - proposed_execpolicy_amendment, - additional_permissions, - }, - header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })), - } +fn build_header(request: &ApprovalRequest) -> Box { + match request { + ApprovalRequest::Exec { + reason, + command, + network_approval_context, + additional_permissions, + .. + } => { + let mut header: Vec> = Vec::new(); + if let Some(reason) = reason { + header.push(Line::from(vec!["Reason: ".into(), reason.clone().italic()])); + header.push(Line::from("")); } - ApprovalRequest::ApplyPatch { - id, - reason, - cwd, - changes, - } => { - let mut header: Vec> = Vec::new(); - if let Some(reason) = reason - && !reason.is_empty() - { - header.push(Box::new( - Paragraph::new(Line::from_iter(["Reason: ".into(), reason.italic()])) - .wrap(Wrap { trim: false }), - )); - header.push(Box::new(Line::from(""))); - } - header.push(DiffSummary::new(changes, cwd).into()); - Self { - variant: ApprovalVariant::ApplyPatch { id }, - header: Box::new(ColumnRenderable::with(header)), - } + if let Some(additional_permissions) = additional_permissions + && let Some(rule_line) = format_additional_permissions_rule(additional_permissions) + { + header.push(Line::from(vec![ + "Permission rule: ".into(), + rule_line.cyan(), + ])); + header.push(Line::from("")); } - ApprovalRequest::McpElicitation { - server_name, - request_id, - message, - } => { - let header = Paragraph::new(vec![ - Line::from(vec!["Server: ".into(), server_name.clone().bold()]), - Line::from(""), - Line::from(message), - ]) - .wrap(Wrap { trim: false }); - Self { - variant: ApprovalVariant::McpElicitation { - server_name, - request_id, - }, - header: Box::new(header), - } + let full_cmd = strip_bash_lc_and_escape(command); + let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd); + if let Some(first) = full_cmd_lines.first_mut() { + first.spans.insert(0, Span::from("$ ")); } + if network_approval_context.is_none() { + header.extend(full_cmd_lines); + } + Box::new(Paragraph::new(header).wrap(Wrap { trim: false })) + } + ApprovalRequest::ApplyPatch { + reason, + cwd, + changes, + .. + } => { + let mut header: Vec> = Vec::new(); + if let Some(reason) = reason + && !reason.is_empty() + { + header.push(Box::new( + Paragraph::new(Line::from_iter([ + "Reason: ".into(), + reason.clone().italic(), + ])) + .wrap(Wrap { trim: false }), + )); + header.push(Box::new(Line::from(""))); + } + header.push(DiffSummary::new(changes.clone(), cwd.clone()).into()); + Box::new(ColumnRenderable::with(header)) + } + ApprovalRequest::McpElicitation { + server_name, + message, + .. + } => { + let header = Paragraph::new(vec![ + Line::from(vec!["Server: ".into(), server_name.clone().bold()]), + Line::from(""), + Line::from(message.clone()), + ]) + .wrap(Wrap { trim: false }); + Box::new(header) } } } -#[derive(Clone)] -enum ApprovalVariant { - Exec { - id: String, - command: Vec, - network_approval_context: Option, - proposed_execpolicy_amendment: Option, - additional_permissions: Option, - }, - ApplyPatch { - id: String, - }, - McpElicitation { - server_name: String, - request_id: RequestId, - }, -} - #[derive(Clone)] enum ApprovalDecision { Review(ReviewDecision), @@ -484,104 +441,116 @@ impl ApprovalOption { } fn exec_options( - proposed_execpolicy_amendment: Option, + available_decisions: &[ReviewDecision], network_approval_context: Option<&NetworkApprovalContext>, - additional_permissions: Option<&AdditionalPermissions>, + additional_permissions: Option<&PermissionProfile>, ) -> Vec { - if network_approval_context.is_some() { - return vec![ - ApprovalOption { - label: "Yes, just this once".to_string(), + available_decisions + .iter() + .filter_map(|decision| match decision { + ReviewDecision::Approved => Some(ApprovalOption { + label: if network_approval_context.is_some() { + "Yes, just this once".to_string() + } else { + "Yes, proceed".to_string() + }, decision: ApprovalDecision::Review(ReviewDecision::Approved), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }, - ApprovalOption { - label: "Yes, and allow this host for this session".to_string(), + }), + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => { + let rendered_prefix = + strip_bash_lc_and_escape(proposed_execpolicy_amendment.command()); + if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') { + return None; + } + + Some(ApprovalOption { + label: format!( + "Yes, and don't ask again for commands that start with `{rendered_prefix}`" + ), + decision: ApprovalDecision::Review( + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: proposed_execpolicy_amendment.clone(), + }, + ), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))], + }) + } + ReviewDecision::ApprovedForSession => Some(ApprovalOption { + label: if network_approval_context.is_some() { + "Yes, and allow this host for this conversation".to_string() + } else if additional_permissions.is_some() { + "Yes, and allow these permissions for this session".to_string() + } else { + "Yes, and don't ask again for this command in this session".to_string() + }, decision: ApprovalDecision::Review(ReviewDecision::ApprovedForSession), display_shortcut: None, additional_shortcuts: vec![key_hint::plain(KeyCode::Char('a'))], - }, - ApprovalOption { - label: "No, and tell Codex what to do differently".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Abort), - display_shortcut: Some(key_hint::plain(KeyCode::Esc)), - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }, - ]; - } - - if additional_permissions.is_some() { - return vec![ - ApprovalOption { - label: "Yes, proceed".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Approved), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }, - ApprovalOption { - label: "No, and tell Codex what to do differently".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Abort), - display_shortcut: Some(key_hint::plain(KeyCode::Esc)), - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }, - ]; - } - - vec![ApprovalOption { - label: "Yes, proceed".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Approved), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('y'))], - }] - .into_iter() - .chain(proposed_execpolicy_amendment.and_then(|prefix| { - let rendered_prefix = strip_bash_lc_and_escape(prefix.command()); - if rendered_prefix.contains('\n') || rendered_prefix.contains('\r') { - return None; - } - - Some(ApprovalOption { - label: format!( - "Yes, and don't ask again for commands that start with `{rendered_prefix}`" - ), - decision: ApprovalDecision::Review(ReviewDecision::ApprovedExecpolicyAmendment { - proposed_execpolicy_amendment: prefix, }), - display_shortcut: None, - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))], + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => { + let (label, shortcut) = match network_policy_amendment.action { + NetworkPolicyRuleAction::Allow => ( + "Yes, and allow this host in the future".to_string(), + KeyCode::Char('p'), + ), + NetworkPolicyRuleAction::Deny => ( + "No, and block this host in the future".to_string(), + KeyCode::Char('d'), + ), + }; + Some(ApprovalOption { + label, + decision: ApprovalDecision::Review(ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.clone(), + }), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(shortcut)], + }) + } + ReviewDecision::Denied => Some(ApprovalOption { + label: "No, continue without running it".to_string(), + decision: ApprovalDecision::Review(ReviewDecision::Denied), + display_shortcut: None, + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('d'))], + }), + ReviewDecision::Abort => Some(ApprovalOption { + label: "No, and tell Codex what to do differently".to_string(), + decision: ApprovalDecision::Review(ReviewDecision::Abort), + display_shortcut: Some(key_hint::plain(KeyCode::Esc)), + additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], + }), }) - })) - .chain([ApprovalOption { - label: "No, and tell Codex what to do differently".to_string(), - decision: ApprovalDecision::Review(ReviewDecision::Abort), - display_shortcut: Some(key_hint::plain(KeyCode::Esc)), - additional_shortcuts: vec![key_hint::plain(KeyCode::Char('n'))], - }]) - .collect() + .collect() } fn format_additional_permissions_rule( - additional_permissions: &AdditionalPermissions, + additional_permissions: &PermissionProfile, ) -> Option { let mut parts = Vec::new(); - if !additional_permissions.fs_read.is_empty() { - let reads = additional_permissions - .fs_read - .iter() - .map(|path| format!("`{}`", path.display())) - .collect::>() - .join(", "); - parts.push(format!("read {reads}")); - } - if !additional_permissions.fs_write.is_empty() { - let writes = additional_permissions - .fs_write - .iter() - .map(|path| format!("`{}`", path.display())) - .collect::>() - .join(", "); - parts.push(format!("write {writes}")); + if let Some(file_system) = additional_permissions.file_system.as_ref() { + if let Some(read) = file_system.read.as_ref() { + let reads = read + .iter() + .map(|path| format!("`{}`", path.display())) + .collect::>() + .join(", "); + parts.push(format!("read {reads}")); + } + if let Some(write) = file_system.write.as_ref() { + let writes = write + .iter() + .map(|path| format!("`{}`", path.display())) + .collect::>() + .join(", "); + parts.push(format!("write {writes}")); + } } if parts.is_empty() { @@ -641,11 +610,19 @@ fn elicitation_options() -> Vec { mod tests { use super::*; use crate::app_event::AppEvent; + use codex_protocol::models::FileSystemPermissions; + use codex_protocol::protocol::ExecPolicyAmendment; use codex_protocol::protocol::NetworkApprovalProtocol; + use codex_protocol::protocol::NetworkPolicyAmendment; + use codex_utils_absolute_path::AbsolutePathBuf; use insta::assert_snapshot; use pretty_assertions::assert_eq; use tokio::sync::mpsc::unbounded_channel; + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(path).expect("absolute path") + } + fn render_overlay_lines(view: &ApprovalOverlay, width: u16) -> String { let height = view.desired_height(width); let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); @@ -662,13 +639,24 @@ mod tests { .join("\n") } + fn normalize_snapshot_paths(rendered: String) -> String { + [ + (absolute_path("/tmp/readme.txt"), "/tmp/readme.txt"), + (absolute_path("/tmp/out.txt"), "/tmp/out.txt"), + ] + .into_iter() + .fold(rendered, |rendered, (path, normalized)| { + rendered.replace(&path.display().to_string(), normalized) + }) + } + fn make_exec_request() -> ApprovalRequest { ApprovalRequest::Exec { id: "test".to_string(), command: vec!["echo".to_string(), "hi".to_string()], reason: Some("reason".to_string()), + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - proposed_execpolicy_amendment: None, additional_permissions: None, } } @@ -711,10 +699,16 @@ mod tests { id: "test".to_string(), command: vec!["echo".to_string()], reason: None, + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![ + "echo".to_string(), + ]), + }, + ReviewDecision::Abort, + ], network_approval_context: None, - proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ - "echo".to_string(), - ])), additional_permissions: None, }, tx, @@ -742,6 +736,43 @@ mod tests { ); } + #[test] + fn network_deny_forever_shortcut_is_not_bound() { + let (tx, mut rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx); + let mut view = ApprovalOverlay::new( + ApprovalRequest::Exec { + id: "test".to_string(), + command: vec!["curl".to_string(), "https://example.com".to_string()], + reason: None, + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, + }, + ReviewDecision::Abort, + ], + network_approval_context: Some(NetworkApprovalContext { + host: "example.com".to_string(), + protocol: NetworkApprovalProtocol::Https, + }), + additional_permissions: None, + }, + tx, + Features::with_defaults(), + ); + view.handle_key_event(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + + assert!( + rx.try_recv().is_err(), + "unexpected approval event emitted for hidden network deny shortcut" + ); + } + #[test] fn header_includes_command_snippet() { let (tx, _rx) = unbounded_channel::(); @@ -751,8 +782,8 @@ mod tests { id: "test".into(), command, reason: None, + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - proposed_execpolicy_amendment: None, additional_permissions: None, }; @@ -782,7 +813,17 @@ mod tests { protocol: NetworkApprovalProtocol::Https, }; let options = exec_options( - Some(ExecPolicyAmendment::new(vec!["curl".to_string()])), + &[ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, + }, + ReviewDecision::Abort, + ], Some(&network_context), None, ); @@ -792,7 +833,31 @@ mod tests { labels, vec![ "Yes, just this once".to_string(), - "Yes, and allow this host for this session".to_string(), + "Yes, and allow this host for this conversation".to_string(), + "Yes, and allow this host in the future".to_string(), + "No, and tell Codex what to do differently".to_string(), + ] + ); + } + + #[test] + fn generic_exec_options_can_offer_allow_for_session() { + let options = exec_options( + &[ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::Abort, + ], + None, + None, + ); + + let labels: Vec = options.into_iter().map(|option| option.label).collect(); + assert_eq!( + labels, + vec![ + "Yes, proceed".to_string(), + "Yes, and don't ask again for this command in this session".to_string(), "No, and tell Codex what to do differently".to_string(), ] ); @@ -800,11 +865,18 @@ mod tests { #[test] fn additional_permissions_exec_options_hide_execpolicy_amendment() { - let additional_permissions = AdditionalPermissions { - fs_read: vec![PathBuf::from("/tmp/readme.txt")], - fs_write: vec![PathBuf::from("/tmp/out.txt")], + let additional_permissions = PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![absolute_path("/tmp/readme.txt")]), + write: Some(vec![absolute_path("/tmp/out.txt")]), + }), + ..Default::default() }; - let options = exec_options(None, None, Some(&additional_permissions)); + let options = exec_options( + &[ReviewDecision::Approved, ReviewDecision::Abort], + None, + Some(&additional_permissions), + ); let labels: Vec = options.into_iter().map(|option| option.label).collect(); assert_eq!( @@ -824,11 +896,14 @@ mod tests { id: "test".into(), command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: None, + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - proposed_execpolicy_amendment: None, - additional_permissions: Some(AdditionalPermissions { - fs_read: vec![PathBuf::from("/tmp/readme.txt")], - fs_write: vec![PathBuf::from("/tmp/out.txt")], + additional_permissions: Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![absolute_path("/tmp/readme.txt")]), + write: Some(vec![absolute_path("/tmp/out.txt")]), + }), + ..Default::default() }), }; @@ -860,18 +935,21 @@ mod tests { id: "test".into(), command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: Some("need filesystem access".into()), + available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - proposed_execpolicy_amendment: None, - additional_permissions: Some(AdditionalPermissions { - fs_read: vec![PathBuf::from("/tmp/readme.txt")], - fs_write: vec![PathBuf::from("/tmp/out.txt")], + additional_permissions: Some(PermissionProfile { + file_system: Some(FileSystemPermissions { + read: Some(vec![absolute_path("/tmp/readme.txt")]), + write: Some(vec![absolute_path("/tmp/out.txt")]), + }), + ..Default::default() }), }; let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults()); assert_snapshot!( "approval_overlay_additional_permissions_prompt", - render_overlay_lines(&view, 120) + normalize_snapshot_paths(render_overlay_lines(&view, 120)) ); } @@ -883,17 +961,28 @@ mod tests { id: "test".into(), command: vec!["curl".into(), "https://example.com".into()], reason: Some("network request blocked".into()), + available_decisions: vec![ + ReviewDecision::Approved, + ReviewDecision::ApprovedForSession, + ReviewDecision::NetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment { + host: "example.com".to_string(), + action: NetworkPolicyRuleAction::Allow, + }, + }, + ReviewDecision::Abort, + ], network_approval_context: Some(NetworkApprovalContext { host: "example.com".to_string(), protocol: NetworkApprovalProtocol::Https, }), - proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec!["curl".into()])), additional_permissions: None, }; let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults()); let mut buf = Buffer::empty(Rect::new(0, 0, 100, view.desired_height(100))); view.render(Rect::new(0, 0, 100, view.desired_height(100)), &mut buf); + assert_snapshot!("network_exec_prompt", format!("{buf:?}")); let rendered: Vec = (0..buf.area.height) .map(|row| { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 7892119966..fe40f4fa7b 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -31,8 +31,8 @@ //! //! # Submission and Prompt Expansion //! -//! When steer is enabled, `Enter` submits immediately. `Tab` requests queuing while a task is -//! running; if no task is running, `Tab` submits just like Enter so input is never dropped. +//! `Enter` submits immediately. `Tab` requests queuing while a task is running; if no task is +//! running, `Tab` submits just like Enter so input is never dropped. //! `Tab` does not submit when entering a `!` shell command. //! //! On submit/queue paths, the composer: @@ -189,6 +189,7 @@ use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX; use codex_protocol::models::local_image_label_text; use codex_protocol::user_input::ByteRange; +use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::TextElement; use codex_utils_fuzzy_match::fuzzy_match; @@ -229,6 +230,12 @@ use tokio::runtime::Handle; /// placeholder in the UI. const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; +fn user_input_too_large_message(actual_chars: usize) -> String { + format!( + "Message exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters ({actual_chars} provided)." + ) +} + /// Result returned when the user interacts with the text area. #[derive(Debug, PartialEq)] pub enum InputResult { @@ -382,14 +389,13 @@ pub(crate) struct ChatComposer { dismissed_mention_popup_token: Option, mention_bindings: HashMap, recent_submission_mention_bindings: Vec, - /// When enabled, `Enter` submits immediately and `Tab` requests queuing behavior. - steer_enabled: bool, collaboration_modes_enabled: bool, config: ChatComposerConfig, collaboration_mode_indicator: Option, connectors_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, + audio_device_selection_enabled: bool, windows_degraded_sandbox_active: bool, status_line_value: Option>, status_line_enabled: bool, @@ -489,13 +495,13 @@ impl ChatComposer { dismissed_mention_popup_token: None, mention_bindings: HashMap::new(), recent_submission_mention_bindings: Vec::new(), - steer_enabled: false, collaboration_modes_enabled: false, config, collaboration_mode_indicator: None, connectors_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, + audio_device_selection_enabled: false, windows_degraded_sandbox_active: false, status_line_value: None, status_line_enabled: false, @@ -550,17 +556,6 @@ impl ChatComposer { ordered } - /// Enables or disables "Steer" behavior for submission keys. - /// - /// When steer is enabled, `Enter` produces [`InputResult::Submitted`] (send immediately) and - /// `Tab` produces [`InputResult::Queued`] when a task is running; otherwise it submits - /// immediately. `Tab` does not submit when the input is a `!` shell command. - /// When steer is disabled, `Enter` produces [`InputResult::Queued`], preserving the default - /// "queue while a task is running" behavior. - pub fn set_steer_enabled(&mut self, enabled: bool) { - self.steer_enabled = enabled; - } - pub fn set_collaboration_modes_enabled(&mut self, enabled: bool) { self.collaboration_modes_enabled = enabled; } @@ -584,6 +579,13 @@ impl ChatComposer { self.realtime_conversation_enabled = enabled; } + pub fn set_audio_device_selection_enabled(&mut self, enabled: bool) { + self.audio_device_selection_enabled = enabled; + } + + /// Compatibility shim for tests that still toggle the removed steer mode flag. + #[cfg(test)] + pub fn set_steer_enabled(&mut self, _enabled: bool) {} pub fn set_voice_transcription_enabled(&mut self, enabled: bool) { self.voice_state.transcription_enabled = enabled; if !enabled { @@ -2267,6 +2269,7 @@ impl ChatComposer { self.connectors_enabled, self.personality_command_enabled, self.realtime_conversation_enabled, + self.audio_device_selection_enabled, self.windows_degraded_sandbox_active, ) .is_some(); @@ -2323,6 +2326,22 @@ impl ChatComposer { text_elements = expanded.text_elements; } } + let actual_chars = text.chars().count(); + if actual_chars > MAX_USER_INPUT_TEXT_CHARS { + let message = user_input_too_large_message(actual_chars); + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_error_event(message), + ))); + self.set_text_content_with_mention_bindings( + original_input.clone(), + original_text_elements, + original_local_image_paths, + original_mention_bindings, + ); + self.pending_pastes.clone_from(&original_pending_pastes); + self.textarea.set_cursor(original_input.len()); + return None; + } // Custom prompt expansion can remove or rewrite image placeholders, so prune any // attachments that no longer have a corresponding placeholder in the expanded text. self.prune_attached_images_for_submission(&text, &text_elements); @@ -2467,6 +2486,7 @@ impl ChatComposer { self.connectors_enabled, self.personality_command_enabled, self.realtime_conversation_enabled, + self.audio_device_selection_enabled, self.windows_degraded_sandbox_active, ) { @@ -2502,6 +2522,7 @@ impl ChatComposer { self.connectors_enabled, self.personality_command_enabled, self.realtime_conversation_enabled, + self.audio_device_selection_enabled, self.windows_degraded_sandbox_active, )?; @@ -2735,25 +2756,12 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, .. - } if self.steer_enabled && !self.is_bang_shell_command() => { - self.handle_submission(self.is_task_running) - } - KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - .. - } if self.is_task_running && !self.is_bang_shell_command() => { - self.handle_submission(true) - } + } if !self.is_bang_shell_command() => self.handle_submission(self.is_task_running), KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. - } => { - let should_queue = !self.steer_enabled; - self.handle_submission(should_queue) - } + } => self.handle_submission(false), input => self.handle_input_basic(input), } } @@ -3153,7 +3161,6 @@ impl ChatComposer { use_shift_enter_hint: self.use_shift_enter_hint, is_task_running: self.is_task_running, quit_shortcut_key: self.quit_shortcut_key, - steer_enabled: self.steer_enabled, collaboration_modes_enabled: self.collaboration_modes_enabled, is_wsl, context_window_percent: self.context_window_percent, @@ -3335,6 +3342,7 @@ impl ChatComposer { self.connectors_enabled, self.personality_command_enabled, self.realtime_conversation_enabled, + self.audio_device_selection_enabled, self.windows_degraded_sandbox_active, ) .is_some(); @@ -3397,6 +3405,7 @@ impl ChatComposer { self.connectors_enabled, self.personality_command_enabled, self.realtime_conversation_enabled, + self.audio_device_selection_enabled, self.windows_degraded_sandbox_active, ) { return true; @@ -3451,6 +3460,7 @@ impl ChatComposer { let connectors_enabled = self.connectors_enabled; let personality_command_enabled = self.personality_command_enabled; let realtime_conversation_enabled = self.realtime_conversation_enabled; + let audio_device_selection_enabled = self.audio_device_selection_enabled; let mut command_popup = CommandPopup::new( self.custom_prompts.clone(), CommandPopupFlags { @@ -3458,6 +3468,7 @@ impl ChatComposer { connectors_enabled, personality_command_enabled, realtime_conversation_enabled, + audio_device_selection_enabled, windows_degraded_sandbox_active: self.windows_degraded_sandbox_active, }, ); @@ -4109,9 +4120,7 @@ impl ChatComposer { | FooterMode::ComposerHasDraft => false, }; let show_queue_hint = match footer_props.mode { - FooterMode::ComposerHasDraft => { - footer_props.is_task_running && footer_props.steer_enabled - } + FooterMode::ComposerHasDraft => footer_props.is_task_running, FooterMode::QuitShortcutReminder | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay @@ -4735,10 +4744,9 @@ mod tests { }, ); - // Textarea has content, agent running, steer enabled: queue hint is shown. + // Textarea has content, agent running: queue hint is shown. snapshot_composer_state_with_width("footer_collapse_queue_full", 120, true, |composer| { setup_collab_footer(composer, 98, None); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }); @@ -4748,7 +4756,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, None); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4759,7 +4766,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, None); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4770,7 +4776,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, None); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4781,20 +4786,18 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, None); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, ); - // Textarea has content, plan mode active, agent running, steer enabled: queue hint + mode. + // Textarea has content, plan mode active, agent running: queue hint + mode. snapshot_composer_state_with_width( "footer_collapse_plan_queue_full", 120, true, |composer| { setup_collab_footer(composer, 98, Some(CollaborationModeIndicator::Plan)); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4805,7 +4808,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, Some(CollaborationModeIndicator::Plan)); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4816,7 +4818,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, Some(CollaborationModeIndicator::Plan)); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4827,7 +4828,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, Some(CollaborationModeIndicator::Plan)); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4838,7 +4838,6 @@ mod tests { true, |composer| { setup_collab_footer(composer, 98, Some(CollaborationModeIndicator::Plan)); - composer.set_steer_enabled(true); composer.set_task_running(true); composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); }, @@ -4910,11 +4909,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); composer.set_text_content("draft text".to_string(), Vec::new(), Vec::new()); assert_eq!(composer.clear_for_ctrl_c(), Some("draft text".to_string())); @@ -4978,7 +4972,6 @@ mod tests { assert_eq!(composer.pending_pastes, vec![(placeholder.clone(), large)]); assert_eq!(composer.textarea.element_payloads(), vec![placeholder]); - composer.set_steer_enabled(true); let (result, _needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { @@ -5132,13 +5125,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let (result, needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); @@ -5181,10 +5167,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); // Force an active paste burst so this test doesn't depend on tight timing. composer @@ -5292,10 +5274,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); assert_eq!(composer.footer_mode, FooterMode::ShortcutOverlay); @@ -5546,7 +5524,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let input = "npx -y @kaeawc/auto-mobile@latest"; composer.textarea.insert_str(input); @@ -5582,10 +5559,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)); assert!(composer.is_in_paste_burst()); @@ -5617,10 +5590,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('あ'), KeyModifiers::NONE)); @@ -5763,9 +5732,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let mut now = Instant::now(); let step = Duration::from_millis(1); @@ -5781,7 +5747,7 @@ mod tests { ); now += step; - let (result, _) = composer.handle_submission_with_time(!composer.steer_enabled, now); + let (result, _) = composer.handle_submission_with_time(false, now); assert!( matches!(result, InputResult::None), "Enter during a burst should insert newline, not submit" @@ -5917,9 +5883,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); let needs_redraw = composer.handle_paste("hello".to_string()); assert!(needs_redraw); @@ -5949,9 +5912,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); - composer.set_steer_enabled(true); // Ensure composer is empty and press Enter. assert!(composer.textarea.text().is_empty()); @@ -5981,7 +5941,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10); let needs_redraw = composer.handle_paste(large.clone()); @@ -6001,6 +5960,118 @@ mod tests { assert!(composer.pending_pastes.is_empty()); } + #[test] + fn submit_at_character_limit_succeeds() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_steer_enabled(true); + let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS); + composer.textarea.set_text_clearing_elements(&input); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(matches!( + result, + InputResult::Submitted { text, .. } if text == input + )); + } + + #[test] + fn oversized_submit_reports_error_and_restores_draft() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, mut rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_steer_enabled(true); + let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); + composer.textarea.set_text_clearing_elements(&input); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(InputResult::None, result); + assert_eq!(composer.textarea.text(), input); + + let mut found_error = false; + while let Ok(event) = rx.try_recv() { + if let AppEvent::InsertHistoryCell(cell) = event { + let message = cell + .display_lines(80) + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + assert!(message.contains(&user_input_too_large_message(input.chars().count()))); + found_error = true; + break; + } + } + assert!(found_error, "expected oversized-input error history cell"); + } + + #[test] + fn oversized_queued_submission_reports_error_and_restores_draft() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, mut rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_steer_enabled(false); + let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); + composer.textarea.set_text_clearing_elements(&input); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(InputResult::None, result); + assert_eq!(composer.textarea.text(), input); + + let mut found_error = false; + while let Ok(event) = rx.try_recv() { + if let AppEvent::InsertHistoryCell(cell) = event { + let message = cell + .display_lines(80) + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + assert!(message.contains(&user_input_too_large_message(input.chars().count()))); + found_error = true; + break; + } + } + assert!(found_error, "expected oversized-input error history cell"); + } + /// Behavior: editing that removes a paste placeholder should also clear the associated /// `pending_pastes` entry so it cannot be submitted accidentally. #[test] @@ -6019,7 +6090,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.handle_paste(large); assert_eq!(composer.pending_pastes.len(), 1); @@ -6153,7 +6223,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); // Type "/mo" humanlike so paste-burst doesn’t interfere. type_chars_humanlike(&mut composer, &['/', 'm', 'o']); @@ -6182,7 +6251,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); type_chars_humanlike(&mut composer, &['/', 'm', 'o']); match &composer.active_popup { @@ -6214,7 +6282,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); // Type "/res" humanlike so paste-burst doesn’t interfere. type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']); @@ -6729,7 +6796,7 @@ mod tests { } #[test] - fn tab_submits_when_no_task_running_in_steer_mode() { + fn tab_submits_when_no_task_running() { use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -6743,7 +6810,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); type_chars_humanlike(&mut composer, &['h', 'i']); @@ -6758,7 +6824,7 @@ mod tests { } #[test] - fn tab_does_not_submit_for_bang_shell_command_in_steer_mode() { + fn tab_does_not_submit_for_bang_shell_command() { use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -6772,7 +6838,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_task_running(false); type_chars_humanlike(&mut composer, &['!', 'l', 's']); @@ -6883,7 +6948,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); // Define test cases: (paste content, is_large) let test_cases = [ @@ -7164,7 +7228,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let path = PathBuf::from("/tmp/image1.png"); composer.attach_image(path.clone()); composer.handle_paste(" hi".into()); @@ -7203,7 +7266,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let mention_bindings = vec![MentionBinding { mention: "figma".to_string(), @@ -7237,7 +7299,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let remote_image_url = "https://example.com/remote.png".to_string(); composer.set_remote_image_urls(vec![remote_image_url.clone()]); let path = PathBuf::from("/tmp/image1.png"); @@ -7304,7 +7365,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); type_chars_humanlike(&mut composer, &['f', 'i', 'r', 's', 't']); let (result, _needs_redraw) = @@ -7370,7 +7430,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let large_content = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 5); composer.handle_paste(large_content.clone()); @@ -7414,7 +7473,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let large_content = format!(" {}", "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 5)); composer.handle_paste(large_content.clone()); @@ -7458,7 +7516,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let pasted = "line1\r\nline2\r\n".to_string(); composer.handle_paste(pasted); @@ -7502,7 +7559,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.textarea.set_text_clearing_elements("/unknown "); composer.textarea.set_cursor("/unknown ".len()); @@ -7549,7 +7605,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); let path = PathBuf::from("/tmp/image2.png"); composer.attach_image(path.clone()); let (result, _) = @@ -7861,7 +7916,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); // Inject prompts as if received via event. composer.set_custom_prompts(vec![CustomPrompt { @@ -7901,7 +7955,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -7937,7 +7990,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -7977,7 +8029,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8033,7 +8084,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8090,7 +8140,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8139,7 +8188,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); // Create a custom prompt with positional args (no named args like $USER) composer.set_custom_prompts(vec![CustomPrompt { @@ -8204,7 +8252,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8263,7 +8310,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer .textarea @@ -8300,7 +8346,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer .textarea @@ -8438,7 +8483,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8477,7 +8521,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8516,7 +8559,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8560,7 +8602,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8601,7 +8642,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(false); composer.set_custom_prompts(vec![CustomPrompt { name: "my-prompt".to_string(), @@ -8616,9 +8656,10 @@ mod tests { .set_text_clearing_elements("/prompts:my-prompt foo "); composer.textarea.set_cursor(composer.textarea.text().len()); composer.attach_image(PathBuf::from("/tmp/unused.png")); + composer.set_task_running(true); let (result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); assert!(matches!( result, @@ -8631,6 +8672,57 @@ mod tests { ); } + #[test] + fn prompt_expansion_over_character_limit_reports_error_and_restores_draft() { + let (tx, mut rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_steer_enabled(true); + + composer.set_custom_prompts(vec![CustomPrompt { + name: "my-prompt".to_string(), + path: "/tmp/my-prompt.md".to_string().into(), + content: "Echo: $1".to_string(), + description: None, + argument_hint: None, + }]); + + let oversized_arg = "x".repeat(MAX_USER_INPUT_TEXT_CHARS); + let original_input = format!("/prompts:my-prompt {oversized_arg}"); + composer + .textarea + .set_text_clearing_elements(&original_input); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(InputResult::None, result); + assert_eq!(composer.textarea.text(), original_input); + + let actual_chars = format!("Echo: {oversized_arg}").chars().count(); + let mut found_error = false; + while let Ok(event) = rx.try_recv() { + if let AppEvent::InsertHistoryCell(cell) = event { + let message = cell + .display_lines(80) + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + assert!(message.contains(&user_input_too_large_message(actual_chars))); + found_error = true; + break; + } + } + assert!(found_error, "expected oversized-input error history cell"); + } + #[test] fn selecting_custom_prompt_with_positional_args_submits_numeric_expansion() { let prompt_text = "Header: $1\nArgs: $ARGUMENTS\n"; @@ -8674,7 +8766,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "elegant".to_string(), @@ -8747,7 +8838,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "price".to_string(), @@ -8786,7 +8876,6 @@ mod tests { "Ask Codex to do anything".to_string(), false, ); - composer.set_steer_enabled(true); composer.set_custom_prompts(vec![CustomPrompt { name: "repeat".to_string(), diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 83b523cc23..62765d9c2c 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -40,6 +40,7 @@ pub(crate) struct CommandPopupFlags { pub(crate) connectors_enabled: bool, pub(crate) personality_command_enabled: bool, pub(crate) realtime_conversation_enabled: bool, + pub(crate) audio_device_selection_enabled: bool, pub(crate) windows_degraded_sandbox_active: bool, } @@ -51,6 +52,7 @@ impl CommandPopup { flags.connectors_enabled, flags.personality_command_enabled, flags.realtime_conversation_enabled, + flags.audio_device_selection_enabled, flags.windows_degraded_sandbox_active, ) .into_iter() @@ -498,6 +500,7 @@ mod tests { connectors_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, + audio_device_selection_enabled: false, windows_degraded_sandbox_active: false, }, ); @@ -518,6 +521,7 @@ mod tests { connectors_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, + audio_device_selection_enabled: false, windows_degraded_sandbox_active: false, }, ); @@ -538,6 +542,7 @@ mod tests { connectors_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, + audio_device_selection_enabled: false, windows_degraded_sandbox_active: false, }, ); @@ -566,6 +571,7 @@ mod tests { connectors_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, + audio_device_selection_enabled: false, windows_degraded_sandbox_active: false, }, ); @@ -577,6 +583,36 @@ mod tests { } } + #[test] + fn settings_command_hidden_when_audio_device_selection_is_disabled() { + let mut popup = CommandPopup::new( + Vec::new(), + CommandPopupFlags { + collaboration_modes_enabled: false, + connectors_enabled: false, + personality_command_enabled: true, + realtime_conversation_enabled: true, + audio_device_selection_enabled: false, + windows_degraded_sandbox_active: false, + }, + ); + popup.on_composer_text_change("/aud".to_string()); + + let cmds: Vec<&str> = popup + .filtered_items() + .into_iter() + .filter_map(|item| match item { + CommandItem::Builtin(cmd) => Some(cmd.command()), + CommandItem::UserPrompt(_) => None, + }) + .collect(); + + assert!( + !cmds.contains(&"settings"), + "expected '/settings' to be hidden when audio device selection is disabled, got {cmds:?}" + ); + } + #[test] fn debug_commands_are_hidden_from_popup() { let popup = CommandPopup::new(Vec::new(), CommandPopupFlags::default()); diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 28005a172a..cf1d302776 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -59,7 +59,6 @@ pub(crate) struct FooterProps { pub(crate) esc_backtrack_hint: bool, pub(crate) use_shift_enter_hint: bool, pub(crate) is_task_running: bool, - pub(crate) steer_enabled: bool, pub(crate) collaboration_modes_enabled: bool, pub(crate) is_wsl: bool, /// Which key the user must press again to quit. @@ -126,8 +125,8 @@ pub(crate) enum FooterMode { ComposerEmpty, /// Base single-line footer when the composer contains a draft. /// - /// The shortcuts hint is suppressed here; when a task is running with - /// steer enabled, this mode can show the queue hint instead. + /// The shortcuts hint is suppressed here; when a task is running, this + /// mode can show the queue hint instead. ComposerHasDraft, } @@ -179,7 +178,7 @@ pub(crate) fn footer_height(props: &FooterProps) -> u16 { | FooterMode::ComposerHasDraft => false, }; let show_queue_hint = match props.mode { - FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::ComposerHasDraft => props.is_task_running, FooterMode::QuitShortcutReminder | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay @@ -1020,7 +1019,7 @@ mod tests { | FooterMode::ComposerHasDraft => false, }; let show_queue_hint = match props.mode { - FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::ComposerHasDraft => props.is_task_running, FooterMode::QuitShortcutReminder | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay @@ -1189,7 +1188,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1207,7 +1205,6 @@ mod tests { esc_backtrack_hint: true, use_shift_enter_hint: true, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1225,7 +1222,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1243,7 +1239,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1261,7 +1256,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1279,7 +1273,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1297,7 +1290,6 @@ mod tests { esc_backtrack_hint: true, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1315,7 +1307,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1333,7 +1324,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1344,24 +1334,6 @@ mod tests { }, ); - snapshot_footer( - "footer_composer_has_draft_queue_hint_disabled", - FooterProps { - mode: FooterMode::ComposerHasDraft, - esc_backtrack_hint: false, - use_shift_enter_hint: false, - is_task_running: true, - steer_enabled: false, - collaboration_modes_enabled: false, - is_wsl: false, - quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), - context_window_percent: None, - context_window_used_tokens: None, - status_line_value: None, - status_line_enabled: false, - }, - ); - snapshot_footer( "footer_composer_has_draft_queue_hint_enabled", FooterProps { @@ -1369,7 +1341,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, - steer_enabled: true, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1385,7 +1356,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1414,7 +1384,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1436,7 +1405,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1453,7 +1421,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1475,7 +1442,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1497,7 +1463,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: false, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1520,7 +1485,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -1547,7 +1511,6 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, - steer_enabled: false, collaboration_modes_enabled: true, is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 5781e055d2..dac0069887 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -17,6 +17,7 @@ use std::path::PathBuf; use crate::app_event::ConnectorsSnapshot; use crate::app_event_sender::AppEventSender; +use crate::bottom_pane::pending_thread_approvals::PendingThreadApprovals; use crate::bottom_pane::queued_user_messages::QueuedUserMessages; use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter; use crate::key_hint; @@ -92,6 +93,7 @@ pub(crate) use skills_toggle_view::SkillsToggleView; pub(crate) use status_line_setup::StatusLineItem; pub(crate) use status_line_setup::StatusLineSetupView; mod paste_burst; +mod pending_thread_approvals; pub mod popup_consts; mod queued_user_messages; mod scroll_state; @@ -171,6 +173,8 @@ pub(crate) struct BottomPane { unified_exec_footer: UnifiedExecFooter, /// Queued user messages to show above the composer while a turn is running. queued_user_messages: QueuedUserMessages, + /// Inactive threads with pending approval requests. + pending_thread_approvals: PendingThreadApprovals, context_window_percent: Option, context_window_used_tokens: Option, } @@ -219,6 +223,7 @@ impl BottomPane { status: None, unified_exec_footer: UnifiedExecFooter::new(), queued_user_messages: QueuedUserMessages::new(), + pending_thread_approvals: PendingThreadApprovals::new(), esc_backtrack_hint: false, animations_enabled, context_window_percent: None, @@ -260,10 +265,6 @@ impl BottomPane { let _ = self.take_mention_bindings(); } - pub fn set_steer_enabled(&mut self, enabled: bool) { - self.composer.set_steer_enabled(enabled); - } - pub fn set_collaboration_modes_enabled(&mut self, enabled: bool) { self.composer.set_collaboration_modes_enabled(enabled); self.request_redraw(); @@ -297,6 +298,11 @@ impl BottomPane { self.request_redraw(); } + pub fn set_audio_device_selection_enabled(&mut self, enabled: bool) { + self.composer.set_audio_device_selection_enabled(enabled); + self.request_redraw(); + } + pub fn set_voice_transcription_enabled(&mut self, enabled: bool) { self.composer.set_voice_transcription_enabled(enabled); self.request_redraw(); @@ -759,6 +765,18 @@ impl BottomPane { self.request_redraw(); } + /// Update the inactive-thread approval list shown above the composer. + pub(crate) fn set_pending_thread_approvals(&mut self, threads: Vec) { + if self.pending_thread_approvals.set_threads(threads) { + self.request_redraw(); + } + } + + #[cfg(test)] + pub(crate) fn pending_thread_approvals(&self) -> &[String] { + self.pending_thread_approvals.threads() + } + /// Update the unified-exec process set and refresh whichever summary surface is active. /// /// The summary may be displayed inline in the status row or as a dedicated @@ -980,14 +998,20 @@ impl BottomPane { if self.status.is_none() && !self.unified_exec_footer.is_empty() { flex.push(0, RenderableItem::Borrowed(&self.unified_exec_footer)); } + let has_pending_thread_approvals = !self.pending_thread_approvals.is_empty(); let has_queued_messages = !self.queued_user_messages.messages.is_empty(); let has_status_or_footer = self.status.is_some() || !self.unified_exec_footer.is_empty(); - if has_queued_messages && has_status_or_footer { + let has_inline_previews = has_pending_thread_approvals || has_queued_messages; + if has_inline_previews && has_status_or_footer { + flex.push(0, RenderableItem::Owned("".into())); + } + flex.push(1, RenderableItem::Borrowed(&self.pending_thread_approvals)); + if has_pending_thread_approvals && has_queued_messages { flex.push(0, RenderableItem::Owned("".into())); } flex.push(1, RenderableItem::Borrowed(&self.queued_user_messages)); - if !has_queued_messages && has_status_or_footer { + if !has_inline_previews && has_status_or_footer { flex.push(0, RenderableItem::Owned("".into())); } let mut flex2 = FlexRenderable::new(); @@ -1094,8 +1118,11 @@ mod tests { id: "1".to_string(), command: vec!["echo".into(), "ok".into()], reason: None, + available_decisions: vec![ + codex_protocol::protocol::ReviewDecision::Approved, + codex_protocol::protocol::ReviewDecision::Abort, + ], network_approval_context: None, - proposed_execpolicy_amendment: None, additional_permissions: None, } } @@ -1494,6 +1521,7 @@ mod tests { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: PathBuf::from("test-skill"), scope: SkillScope::User, diff --git a/codex-rs/tui/src/bottom_pane/pending_thread_approvals.rs b/codex-rs/tui/src/bottom_pane/pending_thread_approvals.rs new file mode 100644 index 0000000000..67a66bfa76 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/pending_thread_approvals.rs @@ -0,0 +1,147 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; + +use crate::render::renderable::Renderable; +use crate::wrapping::RtOptions; +use crate::wrapping::adaptive_wrap_lines; + +/// Widget that lists inactive threads with outstanding approval requests. +pub(crate) struct PendingThreadApprovals { + threads: Vec, +} + +impl PendingThreadApprovals { + pub(crate) fn new() -> Self { + Self { + threads: Vec::new(), + } + } + + pub(crate) fn set_threads(&mut self, threads: Vec) -> bool { + if self.threads == threads { + return false; + } + self.threads = threads; + true + } + + pub(crate) fn is_empty(&self) -> bool { + self.threads.is_empty() + } + + #[cfg(test)] + pub(crate) fn threads(&self) -> &[String] { + &self.threads + } + + fn as_renderable(&self, width: u16) -> Box { + if self.threads.is_empty() || width < 4 { + return Box::new(()); + } + + let mut lines = Vec::new(); + for thread in self.threads.iter().take(3) { + let wrapped = adaptive_wrap_lines( + std::iter::once(Line::from(format!("Approval needed in {thread}"))), + RtOptions::new(width as usize) + .initial_indent(Line::from(vec![" ".into(), "!".red().bold(), " ".into()])) + .subsequent_indent(Line::from(" ")), + ); + lines.extend(wrapped); + } + + if self.threads.len() > 3 { + lines.push(Line::from(" ...".dim().italic())); + } + + lines.push( + Line::from(vec![ + " ".into(), + "/agent".cyan().bold(), + " to switch threads".dim(), + ]) + .dim(), + ); + + Paragraph::new(lines).into() + } +} + +impl Renderable for PendingThreadApprovals { + fn render(&self, area: Rect, buf: &mut Buffer) { + if area.is_empty() { + return; + } + + self.as_renderable(area.width).render(area, buf); + } + + fn desired_height(&self, width: u16) -> u16 { + self.as_renderable(width).desired_height(width) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + use pretty_assertions::assert_eq; + + fn snapshot_rows(widget: &PendingThreadApprovals, width: u16) -> String { + let height = widget.desired_height(width); + let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); + widget.render(Rect::new(0, 0, width, height), &mut buf); + + (0..height) + .map(|y| { + (0..width) + .map(|x| buf[(x, y)].symbol().chars().next().unwrap_or(' ')) + .collect::() + }) + .collect::>() + .join("\n") + } + + #[test] + fn desired_height_empty() { + let widget = PendingThreadApprovals::new(); + assert_eq!(widget.desired_height(40), 0); + } + + #[test] + fn render_single_thread_snapshot() { + let mut widget = PendingThreadApprovals::new(); + widget.set_threads(vec!["Robie [explorer]".to_string()]); + + assert_snapshot!( + snapshot_rows(&widget, 40).replace(' ', "."), + @r" +..!.Approval.needed.in.Robie.[explorer]. +..../agent.to.switch.threads............" + ); + } + + #[test] + fn render_multiple_threads_snapshot() { + let mut widget = PendingThreadApprovals::new(); + widget.set_threads(vec![ + "Main [default]".to_string(), + "Robie [explorer]".to_string(), + "Inspector".to_string(), + "Extra agent".to_string(), + ]); + + assert_snapshot!( + snapshot_rows(&widget, 44).replace(' ', "."), + @r" +..!.Approval.needed.in.Main.[default]....... +..!.Approval.needed.in.Robie.[explorer]..... +..!.Approval.needed.in.Inspector............ +............................................ +..../agent.to.switch.threads................" + ); + } +} diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index 86c131e6db..981c61c451 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -14,6 +14,7 @@ pub(crate) fn builtins_for_input( connectors_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, + audio_device_selection_enabled: bool, allow_elevate_sandbox: bool, ) -> Vec<(&'static str, SlashCommand)> { built_in_slash_commands() @@ -26,6 +27,7 @@ pub(crate) fn builtins_for_input( .filter(|(_, cmd)| connectors_enabled || *cmd != SlashCommand::Apps) .filter(|(_, cmd)| personality_command_enabled || *cmd != SlashCommand::Personality) .filter(|(_, cmd)| realtime_conversation_enabled || *cmd != SlashCommand::Realtime) + .filter(|(_, cmd)| audio_device_selection_enabled || *cmd != SlashCommand::Settings) .collect() } @@ -36,6 +38,7 @@ pub(crate) fn find_builtin_command( connectors_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, + audio_device_selection_enabled: bool, allow_elevate_sandbox: bool, ) -> Option { builtins_for_input( @@ -43,6 +46,7 @@ pub(crate) fn find_builtin_command( connectors_enabled, personality_command_enabled, realtime_conversation_enabled, + audio_device_selection_enabled, allow_elevate_sandbox, ) .into_iter() @@ -57,6 +61,7 @@ pub(crate) fn has_builtin_prefix( connectors_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, + audio_device_selection_enabled: bool, allow_elevate_sandbox: bool, ) -> bool { builtins_for_input( @@ -64,6 +69,7 @@ pub(crate) fn has_builtin_prefix( connectors_enabled, personality_command_enabled, realtime_conversation_enabled, + audio_device_selection_enabled, allow_elevate_sandbox, ) .into_iter() @@ -77,14 +83,14 @@ mod tests { #[test] fn debug_command_still_resolves_for_dispatch() { - let cmd = find_builtin_command("debug-config", true, true, true, false, false); + let cmd = find_builtin_command("debug-config", true, true, true, false, false, false); assert_eq!(cmd, Some(SlashCommand::DebugConfig)); } #[test] fn clear_command_resolves_for_dispatch() { assert_eq!( - find_builtin_command("clear", true, true, true, false, false), + find_builtin_command("clear", true, true, true, false, false, false), Some(SlashCommand::Clear) ); } @@ -92,7 +98,23 @@ mod tests { #[test] fn realtime_command_is_hidden_when_realtime_is_disabled() { assert_eq!( - find_builtin_command("realtime", true, true, true, false, false), + find_builtin_command("realtime", true, true, true, false, true, false), + None + ); + } + + #[test] + fn settings_command_is_hidden_when_realtime_is_disabled() { + assert_eq!( + find_builtin_command("settings", true, true, true, false, false, false), + None + ); + } + + #[test] + fn settings_command_is_hidden_when_audio_device_selection_is_disabled() { + assert_eq!( + find_builtin_command("settings", true, true, true, true, false, false), None ); } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__network_exec_prompt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__network_exec_prompt.snap new file mode 100644 index 0000000000..612563fe1c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__approval_overlay__tests__network_exec_prompt.snap @@ -0,0 +1,38 @@ +--- +source: tui/src/bottom_pane/approval_overlay.rs +assertion_line: 821 +expression: "format!(\"{buf:?}\")" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 100, height: 12 }, + content: [ + " ", + " Do you want to approve network access to "example.com"? ", + " ", + " Reason: network request blocked ", + " ", + " ", + "› 1. Yes, just this once (y) ", + " 2. Yes, and allow this host for this conversation (a) ", + " 3. Yes, and allow this host in the future (p) ", + " 4. No, and tell Codex what to do differently (esc) ", + " ", + " Press enter to confirm or esc to cancel ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 57, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 10, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 33, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 6, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, + x: 28, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 53, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 54, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 45, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 46, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 48, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 51, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + ] +} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap deleted file mode 100644 index 6fdeda07b1..0000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/bottom_pane/footer.rs -expression: terminal.backend() ---- -" 100% context left " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e5ba0bc322..d4f6692296 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -37,6 +37,9 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; +use crate::app_event::RealtimeAudioDeviceKind; +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +use crate::audio_device::list_realtime_audio_device_names; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::StatusLineSetupView; use crate::status::RateLimitWindowDisplay; @@ -850,6 +853,10 @@ impl ChatWidget { && cfg!(not(target_os = "linux")) } + fn realtime_audio_device_selection_enabled(&self) -> bool { + self.realtime_conversation_enabled() && cfg!(feature = "voice-input") + } + /// Synchronize the bottom-pane "task running" indicator with the current lifecycles. /// /// The bottom pane only has one running flag, but this module treats it as a derived state of @@ -2563,12 +2570,13 @@ impl ChatWidget { .unwrap_or_else(|_| ev.command.join(" ")); self.notify(Notification::ExecApprovalRequested { command }); + let available_decisions = ev.effective_available_decisions(); let request = ApprovalRequest::Exec { - id: ev.call_id, + id: ev.effective_approval_id(), command: ev.command, reason: ev.reason, + available_decisions, network_approval_context: ev.network_approval_context, - proposed_execpolicy_amendment: ev.proposed_execpolicy_amendment, additional_permissions: ev.additional_permissions, }; self.bottom_pane @@ -2869,15 +2877,15 @@ impl ChatWidget { }; widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); widget.bottom_pane.set_voice_transcription_enabled( widget.config.features.enabled(Feature::VoiceTranscription), ); widget .bottom_pane .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); + widget + .bottom_pane + .set_audio_device_selection_enabled(widget.realtime_audio_device_selection_enabled()); widget .bottom_pane .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); @@ -3045,15 +3053,15 @@ impl ChatWidget { }; widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); widget.bottom_pane.set_voice_transcription_enabled( widget.config.features.enabled(Feature::VoiceTranscription), ); widget .bottom_pane .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); + widget + .bottom_pane + .set_audio_device_selection_enabled(widget.realtime_audio_device_selection_enabled()); widget .bottom_pane .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); @@ -3211,15 +3219,15 @@ impl ChatWidget { }; widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); widget.bottom_pane.set_voice_transcription_enabled( widget.config.features.enabled(Feature::VoiceTranscription), ); widget .bottom_pane .set_realtime_conversation_enabled(widget.realtime_conversation_enabled()); + widget + .bottom_pane + .set_audio_device_selection_enabled(widget.realtime_audio_device_selection_enabled()); widget .bottom_pane .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); @@ -3357,7 +3365,7 @@ impl ChatWidget { else { return; }; - // Steer submissions during active final-answer streaming can race with turn + // Submissions during active final-answer streaming can race with turn // completion and strand the UI in a running state. Queue those inputs instead // of injecting immediately; `on_task_complete()` drains this FIFO via // `maybe_send_next_queued_input()`, so no typed prompt is dropped. @@ -3365,7 +3373,7 @@ impl ChatWidget { && !self.is_plan_streaming_in_tui() && self.stream_controller.is_none(); if should_submit_now { - // Submitted is only emitted when steer is enabled. + // Submitted is emitted when user submits. // Reset any reasoning header only when we are actually submitting a turn. self.reasoning_buffer.clear(); self.full_reasoning_buffer.clear(); @@ -3543,6 +3551,12 @@ impl ChatWidget { self.start_realtime_conversation(); } } + SlashCommand::Settings => { + if !self.realtime_audio_device_selection_enabled() { + return; + } + self.open_realtime_audio_popup(); + } SlashCommand::Personality => { self.open_personality_popup(); } @@ -4143,7 +4157,7 @@ impl ChatWidget { sandbox_policy: self.config.permissions.sandbox_policy.get().clone(), model: effective_mode.model().to_string(), effort: effective_mode.reasoning_effort(), - summary: self.config.model_reasoning_summary, + summary: None, final_output_json_schema: None, collaboration_mode, personality, @@ -4459,7 +4473,7 @@ impl ChatWidget { | EventMsg::ReasoningContentDelta(_) | EventMsg::ReasoningRawContentDelta(_) | EventMsg::DynamicToolCallRequest(_) - | EventMsg::SkillRequestApproval(_) => {} + | EventMsg::DynamicToolCallResponse(_) => {} EventMsg::RealtimeConversationStarted(ev) => { if !from_replay { self.on_realtime_conversation_started(ev); @@ -4640,6 +4654,10 @@ impl ChatWidget { self.bottom_pane.set_queued_user_messages(messages); } + pub(crate) fn set_pending_thread_approvals(&mut self, threads: Vec) { + self.bottom_pane.set_pending_thread_approvals(threads); + } + pub(crate) fn add_diff_in_progress(&mut self) { self.request_redraw(); } @@ -5270,6 +5288,161 @@ impl ChatWidget { }); } + pub(crate) fn open_realtime_audio_popup(&mut self) { + let items = [ + RealtimeAudioDeviceKind::Microphone, + RealtimeAudioDeviceKind::Speaker, + ] + .into_iter() + .map(|kind| { + let description = Some(format!( + "Current: {}", + self.current_realtime_audio_selection_label(kind) + )); + let actions: Vec = vec![Box::new(move |tx| { + tx.send(AppEvent::OpenRealtimeAudioDeviceSelection { kind }); + })]; + SelectionItem { + name: kind.title().to_string(), + description, + actions, + dismiss_on_select: true, + ..Default::default() + } + }) + .collect(); + + self.bottom_pane.show_selection_view(SelectionViewParams { + title: Some("Settings".to_string()), + subtitle: Some("Configure settings for Codex.".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items, + ..Default::default() + }); + } + + #[cfg(all(not(target_os = "linux"), feature = "voice-input"))] + pub(crate) fn open_realtime_audio_device_selection(&mut self, kind: RealtimeAudioDeviceKind) { + match list_realtime_audio_device_names(kind) { + Ok(device_names) => { + self.open_realtime_audio_device_selection_with_names(kind, device_names); + } + Err(err) => { + self.add_error_message(format!( + "Failed to load realtime {} devices: {err}", + kind.noun() + )); + } + } + } + + #[cfg(any(target_os = "linux", not(feature = "voice-input")))] + pub(crate) fn open_realtime_audio_device_selection(&mut self, kind: RealtimeAudioDeviceKind) { + let _ = kind; + } + + #[cfg(all(not(target_os = "linux"), feature = "voice-input"))] + fn open_realtime_audio_device_selection_with_names( + &mut self, + kind: RealtimeAudioDeviceKind, + device_names: Vec, + ) { + let current_selection = self.current_realtime_audio_device_name(kind); + let current_available = current_selection + .as_deref() + .is_some_and(|name| device_names.iter().any(|device_name| device_name == name)); + let mut items = vec![SelectionItem { + name: "System default".to_string(), + description: Some("Use your operating system default device.".to_string()), + is_current: current_selection.is_none(), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::PersistRealtimeAudioDeviceSelection { kind, name: None }); + })], + dismiss_on_select: true, + ..Default::default() + }]; + + if let Some(selection) = current_selection.as_deref() + && !current_available + { + items.push(SelectionItem { + name: format!("Unavailable: {selection}"), + description: Some("Configured device is not currently available.".to_string()), + is_current: true, + is_disabled: true, + disabled_reason: Some("Reconnect the device or choose another one.".to_string()), + ..Default::default() + }); + } + + items.extend(device_names.into_iter().map(|device_name| { + let persisted_name = device_name.clone(); + let actions: Vec = vec![Box::new(move |tx| { + tx.send(AppEvent::PersistRealtimeAudioDeviceSelection { + kind, + name: Some(persisted_name.clone()), + }); + })]; + SelectionItem { + is_current: current_selection.as_deref() == Some(device_name.as_str()), + name: device_name, + actions, + dismiss_on_select: true, + ..Default::default() + } + })); + + let mut header = ColumnRenderable::new(); + header.push(Line::from(format!("Select {}", kind.title()).bold())); + header.push(Line::from( + "Saved devices apply to realtime voice only.".dim(), + )); + + self.bottom_pane.show_selection_view(SelectionViewParams { + header: Box::new(header), + footer_hint: Some(standard_popup_hint_line()), + items, + ..Default::default() + }); + } + + pub(crate) fn open_realtime_audio_restart_prompt(&mut self, kind: RealtimeAudioDeviceKind) { + let restart_actions: Vec = vec![Box::new(move |tx| { + tx.send(AppEvent::RestartRealtimeAudioDevice { kind }); + })]; + let items = vec![ + SelectionItem { + name: "Restart now".to_string(), + description: Some(format!("Restart local {} audio now.", kind.noun())), + actions: restart_actions, + dismiss_on_select: true, + ..Default::default() + }, + SelectionItem { + name: "Apply later".to_string(), + description: Some(format!( + "Keep the current {} until local audio starts again.", + kind.noun() + )), + dismiss_on_select: true, + ..Default::default() + }, + ]; + + let mut header = ColumnRenderable::new(); + header.push(Line::from(format!("Restart {} now?", kind.title()).bold())); + header.push(Line::from( + "Configuration is saved. Restart local audio to use it immediately.".dim(), + )); + + self.bottom_pane.show_selection_view(SelectionViewParams { + header: Box::new(header), + footer_hint: Some(standard_popup_hint_line()), + items, + ..Default::default() + }); + } + fn model_menu_header(&self, title: &str, subtitle: &str) -> Box { let title = title.to_string(); let subtitle = subtitle.to_string(); @@ -6516,9 +6689,6 @@ impl ChatWidget { } else { self.config.features.disable(feature); } - if feature == Feature::Steer { - self.bottom_pane.set_steer_enabled(enabled); - } if feature == Feature::VoiceTranscription { self.bottom_pane.set_voice_transcription_enabled(enabled); } @@ -6526,6 +6696,8 @@ impl ChatWidget { let realtime_conversation_enabled = self.realtime_conversation_enabled(); self.bottom_pane .set_realtime_conversation_enabled(realtime_conversation_enabled); + self.bottom_pane + .set_audio_device_selection_enabled(self.realtime_audio_device_selection_enabled()); if !realtime_conversation_enabled && self.realtime_conversation.is_live() { self.request_realtime_conversation_close(Some( "Realtime voice mode was closed because the feature was disabled.".to_string(), @@ -6615,6 +6787,17 @@ impl ChatWidget { self.config.personality = Some(personality); } + pub(crate) fn set_realtime_audio_device( + &mut self, + kind: RealtimeAudioDeviceKind, + name: Option, + ) { + match kind { + RealtimeAudioDeviceKind::Microphone => self.config.realtime_audio.microphone = name, + RealtimeAudioDeviceKind::Speaker => self.config.realtime_audio.speaker = name, + } + } + /// Set the syntax theme override in the widget's config copy. pub(crate) fn set_tui_theme(&mut self, theme: Option) { self.config.tui_theme = theme; @@ -6643,6 +6826,22 @@ impl ChatWidget { .unwrap_or_else(|| self.current_collaboration_mode.model()) } + pub(crate) fn realtime_conversation_is_live(&self) -> bool { + self.realtime_conversation.is_active() + } + + fn current_realtime_audio_device_name(&self, kind: RealtimeAudioDeviceKind) -> Option { + match kind { + RealtimeAudioDeviceKind::Microphone => self.config.realtime_audio.microphone.clone(), + RealtimeAudioDeviceKind::Speaker => self.config.realtime_audio.speaker.clone(), + } + } + + fn current_realtime_audio_selection_label(&self, kind: RealtimeAudioDeviceKind) -> String { + self.current_realtime_audio_device_name(kind) + .unwrap_or_else(|| "System default".to_string()) + } + fn sync_personality_command_enabled(&mut self) { self.bottom_pane .set_personality_command_enabled(self.config.features.enabled(Feature::Personality)); @@ -7282,6 +7481,11 @@ impl ChatWidget { self.bottom_pane.remote_image_urls() } + #[cfg(test)] + pub(crate) fn pending_thread_approvals(&self) -> &[String] { + self.bottom_pane.pending_thread_approvals() + } + pub(crate) fn show_esc_backtrack_hint(&mut self) { self.bottom_pane.show_esc_backtrack_hint(); } diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 63d519a7a5..e14a9e3628 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -13,6 +13,17 @@ use tokio::sync::mpsc::unbounded_channel; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; +const TUI_NOTIFY_CLIENT: &str = "codex-tui"; + +async fn initialize_app_server_client_name(thread: &CodexThread) { + if let Err(err) = thread + .set_app_server_client_name(Some(TUI_NOTIFY_CLIENT.to_string())) + .await + { + tracing::error!("failed to set app server client name: {err}"); + } +} + /// Spawn the agent bootstrapper and op forwarding loop, returning the /// `UnboundedSender` used by the UI to submit operations. pub(crate) fn spawn_agent( @@ -42,6 +53,7 @@ pub(crate) fn spawn_agent( return; } }; + initialize_app_server_client_name(thread.as_ref()).await; // Forward the captured `SessionConfigured` event so it can be rendered in the UI. let ev = codex_protocol::protocol::Event { @@ -87,6 +99,8 @@ pub(crate) fn spawn_agent_from_existing( let app_event_tx_clone = app_event_tx; tokio::spawn(async move { + initialize_app_server_client_name(thread.as_ref()).await; + // Forward the captured `SessionConfigured` event so it can be rendered in the UI. let ev = codex_protocol::protocol::Event { id: "".to_string(), @@ -123,6 +137,7 @@ pub(crate) fn spawn_op_forwarder(thread: std::sync::Arc) -> Unbound let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); tokio::spawn(async move { + initialize_app_server_client_name(thread.as_ref()).await; while let Some(op) = codex_op_rx.recv().await { if let Err(e) = thread.submit(op).await { tracing::error!("failed to submit op: {e}"); diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 2cf97188b3..98ac76a34f 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -41,6 +41,10 @@ impl RealtimeConversationUiState { | RealtimeConversationPhase::Stopping ) } + + pub(super) fn is_active(&self) -> bool { + matches!(self.phase, RealtimeConversationPhase::Active) + } } #[derive(Clone, Debug, PartialEq)] @@ -207,7 +211,7 @@ impl ChatWidget { { if self.realtime_conversation.audio_player.is_none() { self.realtime_conversation.audio_player = - crate::voice::RealtimeAudioPlayer::start().ok(); + crate::voice::RealtimeAudioPlayer::start(&self.config).ok(); } if let Some(player) = &self.realtime_conversation.audio_player && let Err(err) = player.enqueue_frame(frame) @@ -231,7 +235,10 @@ impl ChatWidget { self.realtime_conversation.meter_placeholder_id = Some(placeholder_id.clone()); self.request_redraw(); - let capture = match crate::voice::VoiceCapture::start_realtime(self.app_event_tx.clone()) { + let capture = match crate::voice::VoiceCapture::start_realtime( + &self.config, + self.app_event_tx.clone(), + ) { Ok(capture) => capture, Err(err) => { self.remove_transcription_placeholder(&placeholder_id); @@ -250,7 +257,7 @@ impl ChatWidget { self.realtime_conversation.capture = Some(capture); if self.realtime_conversation.audio_player.is_none() { self.realtime_conversation.audio_player = - crate::voice::RealtimeAudioPlayer::start().ok(); + crate::voice::RealtimeAudioPlayer::start(&self.config).ok(); } std::thread::spawn(move || { @@ -275,8 +282,50 @@ impl ChatWidget { #[cfg(target_os = "linux")] fn start_realtime_local_audio(&mut self) {} + #[cfg(all(not(target_os = "linux"), feature = "voice-input"))] + pub(crate) fn restart_realtime_audio_device(&mut self, kind: RealtimeAudioDeviceKind) { + if !self.realtime_conversation.is_active() { + return; + } + + match kind { + RealtimeAudioDeviceKind::Microphone => { + self.stop_realtime_microphone(); + self.start_realtime_local_audio(); + } + RealtimeAudioDeviceKind::Speaker => { + self.stop_realtime_speaker(); + match crate::voice::RealtimeAudioPlayer::start(&self.config) { + Ok(player) => { + self.realtime_conversation.audio_player = Some(player); + } + Err(err) => { + self.add_error_message(format!("Failed to start speaker output: {err}")); + } + } + } + } + self.request_redraw(); + } + + #[cfg(any(target_os = "linux", not(feature = "voice-input")))] + pub(crate) fn restart_realtime_audio_device(&mut self, kind: RealtimeAudioDeviceKind) { + let _ = kind; + } + #[cfg(not(target_os = "linux"))] fn stop_realtime_local_audio(&mut self) { + self.stop_realtime_microphone(); + self.stop_realtime_speaker(); + } + + #[cfg(target_os = "linux")] + fn stop_realtime_local_audio(&mut self) { + self.realtime_conversation.meter_placeholder_id = None; + } + + #[cfg(not(target_os = "linux"))] + fn stop_realtime_microphone(&mut self) { if let Some(flag) = self.realtime_conversation.capture_stop_flag.take() { flag.store(true, Ordering::Relaxed); } @@ -286,13 +335,12 @@ impl ChatWidget { if let Some(id) = self.realtime_conversation.meter_placeholder_id.take() { self.remove_transcription_placeholder(&id); } + } + + #[cfg(not(target_os = "linux"))] + fn stop_realtime_speaker(&mut self) { if let Some(player) = self.realtime_conversation.audio_player.take() { player.clear(); } } - - #[cfg(target_os = "linux")] - fn stop_realtime_local_audio(&mut self) { - self.realtime_conversation.meter_placeholder_id = None; - } } diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index 72af6b89b6..a30e7954f3 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -190,6 +190,7 @@ fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata { .collect(), }), policy: None, + permission_profile: None, permissions: None, path_to_skills_md: skill.path.clone(), scope: skill.scope, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap index 2147714e2e..9b5bb54b9f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__model_selection_popup.snap @@ -5,12 +5,13 @@ expression: popup Select Model and Effort Access legacy models by running codex -m or in your config.toml -› 1. gpt-5.2-codex (default) Frontier agentic coding model. - 2. gpt-5.1-codex-max Codex-optimized flagship for deep and fast +› 1. gpt-5.3-codex (default) Latest frontier agentic coding model. + 2. gpt-5.2-codex Frontier agentic coding model. + 3. gpt-5.1-codex-max Codex-optimized flagship for deep and fast reasoning. - 3. gpt-5.2 Latest frontier model with improvements across + 4. gpt-5.2 Latest frontier model with improvements across knowledge, reasoning and coding - 4. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less + 5. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less capable. Press enter to select reasoning effort, or esc to dismiss. diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap new file mode 100644 index 0000000000..8c60f961f9 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: popup +--- + Settings + Configure settings for Codex. + +› 1. Microphone Current: System default + 2. Speaker Current: System default + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap new file mode 100644 index 0000000000..8c60f961f9 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_audio_selection_popup_narrow.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: popup +--- + Settings + Configure settings for Codex. + +› 1. Microphone Current: System default + 2. Speaker Current: System default + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap new file mode 100644 index 0000000000..3095e6da97 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__realtime_microphone_picker_popup.snap @@ -0,0 +1,18 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: popup +--- + Select Microphone + Saved devices apply to realtime voice only. + + 1. System default Use your operating system + default device. +› 2. Unavailable: Studio Mic (current) (disabled) Configured device is not + currently available. + (disabled: Reconnect the + device or choose another + one.) + 3. Built-in Mic + 4. USB Mic + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index b2ad0cab3c..e748eac585 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -7,6 +7,8 @@ use super::*; use crate::app_event::AppEvent; use crate::app_event::ExitMode; +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +use crate::app_event::RealtimeAudioDeviceKind; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::FeedbackAudience; use crate::bottom_pane::LocalImageAttachment; @@ -23,7 +25,9 @@ use codex_core::config::ConstraintError; #[cfg(target_os = "windows")] use codex_core::config::types::WindowsSandboxModeToml; use codex_core::config_loader::RequirementSource; +use codex_core::features::FEATURES; use codex_core::features::Feature; +use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::models_manager::manager::ModelsManager; use codex_core::skills::model::SkillMetadata; use codex_core::terminal::TerminalName; @@ -749,8 +753,8 @@ async fn enter_with_only_remote_images_submits_user_turn() { chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, + let (items, summary) = match next_submit_op(&mut op_rx) { + Op::UserTurn { items, summary, .. } => (items, summary), other => panic!("expected Op::UserTurn, got {other:?}"), }; assert_eq!( @@ -759,6 +763,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { image_url: remote_url.clone(), }] ); + assert_eq!(summary, None); assert!(chat.remote_image_urls().is_empty()); let mut user_cell = None; @@ -930,6 +935,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: repo_skill_path, scope: SkillScope::Repo, @@ -941,6 +947,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { interface: None, dependencies: None, policy: None, + permission_profile: None, permissions: None, path_to_skills_md: user_skill_path.clone(), scope: SkillScope::User, @@ -1628,12 +1635,16 @@ async fn make_chatwidget_manual( animations_enabled: cfg.animations, skills: None, }); - bottom.set_steer_enabled(true); bottom.set_collaboration_modes_enabled(true); let auth_manager = codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test")); let codex_home = cfg.codex_home.clone(); - let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone(), None)); + let models_manager = Arc::new(ModelsManager::new( + codex_home, + auth_manager.clone(), + None, + CollaborationModesConfig::default(), + )); let reasoning_effort = None; let base_mode = CollaborationMode { mode: ModeKind::Default, @@ -1758,6 +1769,7 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.config.codex_home.clone(), chat.auth_manager.clone(), None, + CollaborationModesConfig::default(), )); } @@ -2780,6 +2792,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -2810,6 +2823,44 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { ); } +#[tokio::test] +async fn exec_approval_uses_approval_id_when_present() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + + chat.handle_codex_event(Event { + id: "sub-short".into(), + msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + call_id: "call-parent".into(), + approval_id: Some("approval-subcommand".into()), + turn_id: "turn-short".into(), + command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], + cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + reason: Some( + "this is a test reason such as one that would be produced by the model".into(), + ), + network_approval_context: None, + proposed_execpolicy_amendment: None, + proposed_network_policy_amendments: None, + additional_permissions: None, + available_decisions: None, + parsed_cmd: vec![], + }), + }); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + + let mut found = false; + while let Ok(app_ev) = rx.try_recv() { + if let AppEvent::CodexOp(Op::ExecApproval { id, decision, .. }) = app_ev { + assert_eq!(id, "approval-subcommand"); + assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved); + found = true; + break; + } + } + assert!(found, "expected ExecApproval op to be sent"); +} + #[tokio::test] async fn exec_approval_decision_truncates_multiline_and_long_commands() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; @@ -2828,6 +2879,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -2882,6 +2934,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -3387,7 +3440,7 @@ async fn unified_exec_begin_restores_working_status_snapshot() { } #[tokio::test] -async fn steer_enter_queues_while_plan_stream_is_active() { +async fn enter_queues_while_plan_stream_is_active() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; chat.thread_id = Some(ThreadId::new()); chat.set_feature_enabled(Feature::CollaborationModes, true); @@ -3511,7 +3564,7 @@ async fn steer_enter_during_final_stream_preserves_follow_up_prompts_in_order() } #[tokio::test] -async fn steer_enter_submits_when_plan_stream_is_not_active() { +async fn enter_submits_when_plan_stream_is_not_active() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; chat.thread_id = Some(ThreadId::new()); chat.set_feature_enabled(Feature::CollaborationModes, true); @@ -5905,6 +5958,30 @@ async fn experimental_features_toggle_saves_on_exit() { assert_eq!(updates, vec![(expected_feature, true)]); } +#[tokio::test] +async fn experimental_popup_shows_js_repl_node_requirement() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; + + let js_repl_description = FEATURES + .iter() + .find(|spec| spec.id == Feature::JsRepl) + .and_then(|spec| spec.stage.experimental_menu_description()) + .expect("expected js_repl experimental description"); + let node_requirement = js_repl_description + .split(". ") + .find(|sentence| sentence.starts_with("Requires Node >= v")) + .map(|sentence| sentence.trim_end_matches(" installed.")) + .expect("expected js_repl description to mention the Node requirement"); + + chat.open_experimental_popup(); + + let popup = render_bottom_popup(&chat, 120); + assert!( + popup.contains(node_requirement), + "expected js_repl feature description to mention the required Node version, got:\n{popup}" + ); +} + #[tokio::test] async fn model_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5-codex")).await; @@ -5925,6 +6002,62 @@ async fn personality_selection_popup_snapshot() { assert_snapshot!("personality_selection_popup", popup); } +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +#[tokio::test] +async fn realtime_audio_selection_popup_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await; + chat.open_realtime_audio_popup(); + + let popup = render_bottom_popup(&chat, 80); + assert_snapshot!("realtime_audio_selection_popup", popup); +} + +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +#[tokio::test] +async fn realtime_audio_selection_popup_narrow_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await; + chat.open_realtime_audio_popup(); + + let popup = render_bottom_popup(&chat, 56); + assert_snapshot!("realtime_audio_selection_popup_narrow", popup); +} + +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +#[tokio::test] +async fn realtime_microphone_picker_popup_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await; + chat.config.realtime_audio.microphone = Some("Studio Mic".to_string()); + chat.open_realtime_audio_device_selection_with_names( + RealtimeAudioDeviceKind::Microphone, + vec!["Built-in Mic".to_string(), "USB Mic".to_string()], + ); + + let popup = render_bottom_popup(&chat, 80); + assert_snapshot!("realtime_microphone_picker_popup", popup); +} + +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +#[tokio::test] +async fn realtime_audio_picker_emits_persist_event() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await; + chat.open_realtime_audio_device_selection_with_names( + RealtimeAudioDeviceKind::Speaker, + vec!["Desk Speakers".to_string(), "Headphones".to_string()], + ); + + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_matches!( + rx.try_recv(), + Ok(AppEvent::PersistRealtimeAudioDeviceSelection { + kind: RealtimeAudioDeviceKind::Speaker, + name: Some(name), + }) if name == "Headphones" + ); +} + #[tokio::test] async fn model_picker_hides_show_in_picker_false_models_from_cache() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("test-visible-model")).await; @@ -5943,6 +6076,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() { is_default: false, upgrade: None, show_in_picker, + availability_nux: None, supported_in_api: true, input_modalities: default_input_modalities(), }; @@ -6211,6 +6345,7 @@ async fn single_reasoning_option_skips_selection() { is_default: false, upgrade: None, show_in_picker: true, + availability_nux: None, supported_in_api: true, input_modalities: default_input_modalities(), }; @@ -6677,6 +6812,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -6737,6 +6873,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -6784,6 +6921,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { @@ -7150,6 +7288,7 @@ async fn status_widget_and_approval_modal_snapshot() { ])), proposed_network_policy_amendments: None, additional_permissions: None, + available_decisions: None, parsed_cmd: vec![], }; chat.handle_codex_event(Event { diff --git a/codex-rs/tui/src/diff_render.rs b/codex-rs/tui/src/diff_render.rs index 32591945fa..d779541fcd 100644 --- a/codex-rs/tui/src/diff_render.rs +++ b/codex-rs/tui/src/diff_render.rs @@ -83,6 +83,8 @@ use crate::terminal_palette::indexed_color; use crate::terminal_palette::rgb_color; use crate::terminal_palette::stdout_color_level; use codex_core::git_info::get_git_repo_root; +use codex_core::terminal::TerminalName; +use codex_core::terminal::terminal_info; use codex_protocol::protocol::FileChange; /// Classifies a diff line for gutter sign rendering and style selection. @@ -110,6 +112,14 @@ enum DiffTheme { Light, } +/// Palette depth the diff renderer will target. +/// +/// This is the *renderer's own* notion of color depth, derived from — but not +/// identical to — the raw [`StdoutColorLevel`] reported by `supports-color`. +/// The indirection exists because some terminals (notably Windows Terminal) +/// advertise only ANSI-16 support while actually rendering truecolor sequences +/// correctly; [`diff_color_level_for_terminal`] promotes those cases so the +/// diff output uses the richer palette. #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum DiffColorLevel { TrueColor, @@ -117,6 +127,22 @@ enum DiffColorLevel { Ansi16, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RichDiffColorLevel { + TrueColor, + Ansi256, +} + +impl RichDiffColorLevel { + fn from_diff_color_level(level: DiffColorLevel) -> Option { + match level { + DiffColorLevel::TrueColor => Some(Self::TrueColor), + DiffColorLevel::Ansi256 => Some(Self::Ansi256), + DiffColorLevel::Ansi16 => None, + } + } +} + pub struct DiffSummary { changes: HashMap, cwd: PathBuf, @@ -876,11 +902,75 @@ fn diff_theme() -> DiffTheme { diff_theme_for_bg(default_bg()) } +/// Return the [`DiffColorLevel`] for the current terminal session. +/// +/// This is the environment-reading adapter: it samples runtime signals +/// (`supports-color` level, terminal name, `WT_SESSION`, and `FORCE_COLOR`) +/// and forwards them to [`diff_color_level_for_terminal`]. +/// +/// Keeping env reads in this thin wrapper lets +/// [`diff_color_level_for_terminal`] stay pure and easy to unit test. fn diff_color_level() -> DiffColorLevel { - match stdout_color_level() { + diff_color_level_for_terminal( + stdout_color_level(), + terminal_info().name, + std::env::var_os("WT_SESSION").is_some(), + has_force_color_override(), + ) +} + +/// Returns whether `FORCE_COLOR` is explicitly set. +fn has_force_color_override() -> bool { + std::env::var_os("FORCE_COLOR").is_some() +} + +/// Map a raw [`StdoutColorLevel`] to a [`DiffColorLevel`] using +/// Windows Terminal-specific truecolor promotion rules. +/// +/// This helper is intentionally pure (no env access) so tests can validate +/// the policy table by passing explicit inputs. +/// +/// Windows Terminal fully supports 24-bit color but the `supports-color` +/// crate often reports only ANSI-16 there because no `COLORTERM` variable +/// is set. We detect Windows Terminal two ways — via `terminal_name` +/// (parsed from `WT_SESSION` / `TERM_PROGRAM` by `terminal_info()`) and +/// via the raw `has_wt_session` flag. +/// +/// These signals are intentionally not equivalent: `terminal_name` is a +/// derived classification with `TERM_PROGRAM` precedence, so `WT_SESSION` +/// can be present while `terminal_name` is not `WindowsTerminal`. +/// +/// When `WT_SESSION` is present, we promote to truecolor unconditionally +/// unless `FORCE_COLOR` is set. This keeps Windows Terminal rendering rich +/// by default while preserving explicit `FORCE_COLOR` user intent. +/// +/// Outside `WT_SESSION`, only ANSI-16 is promoted for identified +/// `WindowsTerminal` sessions; `Unknown` stays conservative. +fn diff_color_level_for_terminal( + stdout_level: StdoutColorLevel, + terminal_name: TerminalName, + has_wt_session: bool, + has_force_color_override: bool, +) -> DiffColorLevel { + if has_wt_session && !has_force_color_override { + return DiffColorLevel::TrueColor; + } + + let base = match stdout_level { StdoutColorLevel::TrueColor => DiffColorLevel::TrueColor, StdoutColorLevel::Ansi256 => DiffColorLevel::Ansi256, StdoutColorLevel::Ansi16 | StdoutColorLevel::Unknown => DiffColorLevel::Ansi16, + }; + + // Outside `WT_SESSION`, keep the existing Windows Terminal promotion for + // ANSI-16 sessions that likely support truecolor. + if stdout_level == StdoutColorLevel::Ansi16 + && terminal_name == TerminalName::WindowsTerminal + && !has_force_color_override + { + DiffColorLevel::TrueColor + } else { + base } } @@ -908,10 +998,11 @@ fn diff_color_level() -> DiffColorLevel { /// Context lines intentionally leave the background unset so the terminal /// default shows through. fn style_line_bg_for(kind: DiffLineType, theme: DiffTheme, color_level: DiffColorLevel) -> Style { - match kind { - DiffLineType::Insert => Style::default().bg(add_line_bg(theme, color_level)), - DiffLineType::Delete => Style::default().bg(del_line_bg(theme, color_level)), - DiffLineType::Context => Style::default(), + match (kind, RichDiffColorLevel::from_diff_color_level(color_level)) { + (_, None) => Style::default(), + (DiffLineType::Insert, Some(level)) => Style::default().bg(add_line_bg(theme, level)), + (DiffLineType::Delete, Some(level)) => Style::default().bg(del_line_bg(theme, level)), + (DiffLineType::Context, _) => Style::default(), } } @@ -919,25 +1010,21 @@ fn style_context() -> Style { Style::default() } -fn add_line_bg(theme: DiffTheme, color_level: DiffColorLevel) -> Color { +fn add_line_bg(theme: DiffTheme, color_level: RichDiffColorLevel) -> Color { match (theme, color_level) { - (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb_color(DARK_TC_ADD_LINE_BG_RGB), - (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed_color(DARK_256_ADD_LINE_BG_IDX), - (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Green, - (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb_color(LIGHT_TC_ADD_LINE_BG_RGB), - (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed_color(LIGHT_256_ADD_LINE_BG_IDX), - (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::LightGreen, + (DiffTheme::Dark, RichDiffColorLevel::TrueColor) => rgb_color(DARK_TC_ADD_LINE_BG_RGB), + (DiffTheme::Dark, RichDiffColorLevel::Ansi256) => indexed_color(DARK_256_ADD_LINE_BG_IDX), + (DiffTheme::Light, RichDiffColorLevel::TrueColor) => rgb_color(LIGHT_TC_ADD_LINE_BG_RGB), + (DiffTheme::Light, RichDiffColorLevel::Ansi256) => indexed_color(LIGHT_256_ADD_LINE_BG_IDX), } } -fn del_line_bg(theme: DiffTheme, color_level: DiffColorLevel) -> Color { +fn del_line_bg(theme: DiffTheme, color_level: RichDiffColorLevel) -> Color { match (theme, color_level) { - (DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb_color(DARK_TC_DEL_LINE_BG_RGB), - (DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed_color(DARK_256_DEL_LINE_BG_IDX), - (DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Red, - (DiffTheme::Light, DiffColorLevel::TrueColor) => rgb_color(LIGHT_TC_DEL_LINE_BG_RGB), - (DiffTheme::Light, DiffColorLevel::Ansi256) => indexed_color(LIGHT_256_DEL_LINE_BG_IDX), - (DiffTheme::Light, DiffColorLevel::Ansi16) => Color::LightRed, + (DiffTheme::Dark, RichDiffColorLevel::TrueColor) => rgb_color(DARK_TC_DEL_LINE_BG_RGB), + (DiffTheme::Dark, RichDiffColorLevel::Ansi256) => indexed_color(DARK_256_DEL_LINE_BG_IDX), + (DiffTheme::Light, RichDiffColorLevel::TrueColor) => rgb_color(LIGHT_TC_DEL_LINE_BG_RGB), + (DiffTheme::Light, RichDiffColorLevel::Ansi256) => indexed_color(LIGHT_256_DEL_LINE_BG_IDX), } } @@ -949,19 +1036,17 @@ fn light_gutter_fg(color_level: DiffColorLevel) -> Color { } } -fn light_add_num_bg(color_level: DiffColorLevel) -> Color { +fn light_add_num_bg(color_level: RichDiffColorLevel) -> Color { match color_level { - DiffColorLevel::TrueColor => rgb_color(LIGHT_TC_ADD_NUM_BG_RGB), - DiffColorLevel::Ansi256 => indexed_color(LIGHT_256_ADD_NUM_BG_IDX), - DiffColorLevel::Ansi16 => Color::Green, + RichDiffColorLevel::TrueColor => rgb_color(LIGHT_TC_ADD_NUM_BG_RGB), + RichDiffColorLevel::Ansi256 => indexed_color(LIGHT_256_ADD_NUM_BG_IDX), } } -fn light_del_num_bg(color_level: DiffColorLevel) -> Color { +fn light_del_num_bg(color_level: RichDiffColorLevel) -> Color { match color_level { - DiffColorLevel::TrueColor => rgb_color(LIGHT_TC_DEL_NUM_BG_RGB), - DiffColorLevel::Ansi256 => indexed_color(LIGHT_256_DEL_NUM_BG_IDX), - DiffColorLevel::Ansi16 => Color::Red, + RichDiffColorLevel::TrueColor => rgb_color(LIGHT_TC_DEL_NUM_BG_RGB), + RichDiffColorLevel::Ansi256 => indexed_color(LIGHT_256_DEL_NUM_BG_IDX), } } @@ -969,13 +1054,23 @@ fn light_del_num_bg(color_level: DiffColorLevel) -> Color { /// tinted background so numbers contrast against the pastel line fill. On /// dark backgrounds a simple `DIM` modifier is sufficient. fn style_gutter_for(kind: DiffLineType, theme: DiffTheme, color_level: DiffColorLevel) -> Style { - match (theme, kind) { - (DiffTheme::Light, DiffLineType::Insert) => Style::default() + match ( + theme, + kind, + RichDiffColorLevel::from_diff_color_level(color_level), + ) { + (DiffTheme::Light, DiffLineType::Insert, None) => { + Style::default().fg(light_gutter_fg(color_level)) + } + (DiffTheme::Light, DiffLineType::Delete, None) => { + Style::default().fg(light_gutter_fg(color_level)) + } + (DiffTheme::Light, DiffLineType::Insert, Some(level)) => Style::default() .fg(light_gutter_fg(color_level)) - .bg(light_add_num_bg(color_level)), - (DiffTheme::Light, DiffLineType::Delete) => Style::default() + .bg(light_add_num_bg(level)), + (DiffTheme::Light, DiffLineType::Delete, Some(level)) => Style::default() .fg(light_gutter_fg(color_level)) - .bg(light_del_num_bg(color_level)), + .bg(light_del_num_bg(level)), _ => style_gutter_dim(), } } @@ -1001,26 +1096,38 @@ fn style_sign_del(theme: DiffTheme, color_level: DiffColorLevel) -> Style { /// Content style for insert lines (plain, non-syntax-highlighted text). fn style_add(theme: DiffTheme, color_level: DiffColorLevel) -> Style { match (theme, color_level) { - (DiffTheme::Dark, DiffColorLevel::Ansi16) => Style::default() - .fg(Color::Black) - .bg(add_line_bg(theme, color_level)), - (DiffTheme::Light, _) => Style::default().bg(add_line_bg(theme, color_level)), - (DiffTheme::Dark, _) => Style::default() + (_, DiffColorLevel::Ansi16) => Style::default().fg(Color::Green), + (DiffTheme::Light, DiffColorLevel::TrueColor) => { + Style::default().bg(add_line_bg(theme, RichDiffColorLevel::TrueColor)) + } + (DiffTheme::Light, DiffColorLevel::Ansi256) => { + Style::default().bg(add_line_bg(theme, RichDiffColorLevel::Ansi256)) + } + (DiffTheme::Dark, DiffColorLevel::TrueColor) => Style::default() .fg(Color::Green) - .bg(add_line_bg(theme, color_level)), + .bg(add_line_bg(theme, RichDiffColorLevel::TrueColor)), + (DiffTheme::Dark, DiffColorLevel::Ansi256) => Style::default() + .fg(Color::Green) + .bg(add_line_bg(theme, RichDiffColorLevel::Ansi256)), } } /// Content style for delete lines (plain, non-syntax-highlighted text). fn style_del(theme: DiffTheme, color_level: DiffColorLevel) -> Style { match (theme, color_level) { - (DiffTheme::Dark, DiffColorLevel::Ansi16) => Style::default() - .fg(Color::Black) - .bg(del_line_bg(theme, color_level)), - (DiffTheme::Light, _) => Style::default().bg(del_line_bg(theme, color_level)), - (DiffTheme::Dark, _) => Style::default() + (_, DiffColorLevel::Ansi16) => Style::default().fg(Color::Red), + (DiffTheme::Light, DiffColorLevel::TrueColor) => { + Style::default().bg(del_line_bg(theme, RichDiffColorLevel::TrueColor)) + } + (DiffTheme::Light, DiffColorLevel::Ansi256) => { + Style::default().bg(del_line_bg(theme, RichDiffColorLevel::Ansi256)) + } + (DiffTheme::Dark, DiffColorLevel::TrueColor) => Style::default() .fg(Color::Red) - .bg(del_line_bg(theme, color_level)), + .bg(del_line_bg(theme, RichDiffColorLevel::TrueColor)), + (DiffTheme::Dark, DiffColorLevel::Ansi256) => Style::default() + .fg(Color::Red) + .bg(del_line_bg(theme, RichDiffColorLevel::Ansi256)), } } @@ -1039,32 +1146,28 @@ mod tests { use ratatui::widgets::Paragraph; #[test] - fn dark_ansi16_add_style_has_contrast() { + fn ansi16_add_style_uses_foreground_only() { let style = style_add(DiffTheme::Dark, DiffColorLevel::Ansi16); - assert_eq!(style.fg, Some(Color::Black)); - assert_eq!(style.bg, Some(Color::Green)); - assert_ne!(style.fg, style.bg); + assert_eq!(style.fg, Some(Color::Green)); + assert_eq!(style.bg, None); } #[test] - fn dark_ansi16_del_style_has_contrast() { + fn ansi16_del_style_uses_foreground_only() { let style = style_del(DiffTheme::Dark, DiffColorLevel::Ansi16); - assert_eq!(style.fg, Some(Color::Black)); - assert_eq!(style.bg, Some(Color::Red)); - assert_ne!(style.fg, style.bg); + assert_eq!(style.fg, Some(Color::Red)); + assert_eq!(style.bg, None); } #[test] - fn dark_ansi16_sign_styles_have_contrast() { + fn ansi16_sign_styles_use_foreground_only() { let add_sign = style_sign_add(DiffTheme::Dark, DiffColorLevel::Ansi16); - assert_eq!(add_sign.fg, Some(Color::Black)); - assert_eq!(add_sign.bg, Some(Color::Green)); - assert_ne!(add_sign.fg, add_sign.bg); + assert_eq!(add_sign.fg, Some(Color::Green)); + assert_eq!(add_sign.bg, None); let del_sign = style_sign_del(DiffTheme::Dark, DiffColorLevel::Ansi16); - assert_eq!(del_sign.fg, Some(Color::Black)); - assert_eq!(del_sign.bg, Some(Color::Red)); - assert_ne!(del_sign.fg, del_sign.bg); + assert_eq!(del_sign.fg, Some(Color::Red)); + assert_eq!(del_sign.bg, None); } use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; @@ -1466,6 +1569,32 @@ mod tests { snapshot_diff_gallery("diff_gallery_120x40", 120, 40); } + #[test] + fn ui_snapshot_ansi16_insert_delete_no_background() { + let mut lines = push_wrapped_diff_line_inner_with_theme_and_color_level( + 1, + DiffLineType::Insert, + "added in ansi16 mode", + 80, + line_number_width(2), + None, + DiffTheme::Dark, + DiffColorLevel::Ansi16, + ); + lines.extend(push_wrapped_diff_line_inner_with_theme_and_color_level( + 2, + DiffLineType::Delete, + "deleted in ansi16 mode", + 80, + line_number_width(2), + None, + DiffTheme::Dark, + DiffColorLevel::Ansi16, + )); + + snapshot_lines("ansi16_insert_delete_no_background", lines, 40, 4); + } + #[test] fn truecolor_dark_theme_uses_configured_backgrounds() { assert_eq!( @@ -1535,6 +1664,42 @@ mod tests { ); } + #[test] + fn ansi16_disables_line_and_gutter_backgrounds() { + assert_eq!( + style_line_bg_for( + DiffLineType::Insert, + DiffTheme::Dark, + DiffColorLevel::Ansi16 + ), + Style::default() + ); + assert_eq!( + style_line_bg_for( + DiffLineType::Delete, + DiffTheme::Light, + DiffColorLevel::Ansi16 + ), + Style::default() + ); + assert_eq!( + style_gutter_for( + DiffLineType::Insert, + DiffTheme::Light, + DiffColorLevel::Ansi16 + ), + Style::default().fg(Color::Black) + ); + assert_eq!( + style_gutter_for( + DiffLineType::Delete, + DiffTheme::Light, + DiffColorLevel::Ansi16 + ), + Style::default().fg(Color::Black) + ); + } + #[test] fn light_truecolor_theme_uses_readable_gutter_and_line_backgrounds() { assert_eq!( @@ -1608,6 +1773,97 @@ mod tests { assert_eq!(lines[1].style.bg, Some(rgb_color(LIGHT_TC_ADD_LINE_BG_RGB))); } + #[test] + fn windows_terminal_promotes_ansi16_to_truecolor_for_diffs() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Ansi16, + TerminalName::WindowsTerminal, + false, + false, + ), + DiffColorLevel::TrueColor + ); + } + + #[test] + fn wt_session_promotes_ansi16_to_truecolor_for_diffs() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Ansi16, + TerminalName::Unknown, + true, + false, + ), + DiffColorLevel::TrueColor + ); + } + + #[test] + fn non_windows_terminal_keeps_ansi16_diff_palette() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Ansi16, + TerminalName::WezTerm, + false, + false, + ), + DiffColorLevel::Ansi16 + ); + } + + #[test] + fn wt_session_promotes_unknown_color_level_to_truecolor() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Unknown, + TerminalName::WindowsTerminal, + true, + false, + ), + DiffColorLevel::TrueColor + ); + } + + #[test] + fn non_wt_windows_terminal_keeps_unknown_color_level_conservative() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Unknown, + TerminalName::WindowsTerminal, + false, + false, + ), + DiffColorLevel::Ansi16 + ); + } + + #[test] + fn explicit_force_override_keeps_ansi16_on_windows_terminal() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Ansi16, + TerminalName::WindowsTerminal, + false, + true, + ), + DiffColorLevel::Ansi16 + ); + } + + #[test] + fn explicit_force_override_keeps_ansi256_on_windows_terminal() { + assert_eq!( + diff_color_level_for_terminal( + StdoutColorLevel::Ansi256, + TerminalName::WindowsTerminal, + true, + true, + ), + DiffColorLevel::Ansi256 + ); + } + #[test] fn add_diff_uses_path_extension_for_highlighting() { let mut changes: HashMap = HashMap::new(); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index c46fc60232..bef336e3a3 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -791,6 +791,7 @@ pub fn new_approval_decision_cell( command: Vec, decision: codex_protocol::protocol::ReviewDecision, ) -> Box { + use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision::*; let (symbol, summary): (Span<'static>, Vec>) = match decision { @@ -836,29 +837,27 @@ pub fn new_approval_decision_cell( } NetworkPolicyAmendment { network_policy_amendment, - } => { - let host = Span::from(network_policy_amendment.host).dim(); - match network_policy_amendment.action { - codex_protocol::protocol::NetworkPolicyRuleAction::Allow => ( - "✔ ".green(), - vec![ - "You ".into(), - "approved".bold(), - " future network access to ".into(), - host, - ], - ), - codex_protocol::protocol::NetworkPolicyRuleAction::Deny => ( - "✗ ".red(), - vec![ - "You ".into(), - "blocked".bold(), - " future network access to ".into(), - host, - ], - ), - } - } + } => match network_policy_amendment.action { + NetworkPolicyRuleAction::Allow => ( + "✔ ".green(), + vec![ + "You ".into(), + "persisted".bold(), + " Codex network access to ".into(), + Span::from(network_policy_amendment.host).dim(), + ], + ), + NetworkPolicyRuleAction::Deny => ( + "✗ ".red(), + vec![ + "You ".into(), + "denied".bold(), + " codex network access to ".into(), + Span::from(network_policy_amendment.host).dim(), + " and saved that rule".into(), + ], + ), + }, Denied => { let snippet = Span::from(exec_snippet(&command)).dim(); ( @@ -2600,6 +2599,16 @@ mod tests { insta::assert_snapshot!(rendered); } + #[test] + fn error_event_oversized_input_snapshot() { + let cell = new_error_event( + "Message exceeds the maximum length of 1048576 characters (1048577 provided)." + .to_string(), + ); + let rendered = render_lines(&cell.display_lines(120)).join("\n"); + insta::assert_snapshot!(rendered); + } + #[tokio::test] async fn mcp_tools_output_masks_sensitive_values() { let mut config = test_config().await; @@ -2621,6 +2630,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }; let mut servers = config.mcp_servers.get().clone(); servers.insert("docs".to_string(), stdio_config); @@ -2644,6 +2654,7 @@ mod tests { enabled_tools: None, disabled_tools: None, scopes: None, + oauth_resource: None, }; servers.insert("http".to_string(), http_config); config diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 52c5cd9e5c..8dfdbcd871 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -31,8 +31,10 @@ use codex_core::find_thread_path_by_name_str; use codex_core::format_exec_policy_error_with_source; use codex_core::path_utils; use codex_core::read_session_meta_line; +use codex_core::state_db::get_state_db; use codex_core::terminal::Multiplexer; use codex_core::windows_sandbox::WindowsSandboxLevelExt; +use codex_protocol::ThreadId; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WindowsSandboxLevel; @@ -61,6 +63,8 @@ mod app_backtrack; mod app_event; mod app_event_sender; mod ascii_animation; +#[cfg(all(not(target_os = "linux"), feature = "voice-input"))] +mod audio_device; mod bottom_pane; mod chatwidget; mod cli; @@ -121,6 +125,7 @@ mod voice; mod voice { use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; + use codex_core::config::Config; use codex_protocol::protocol::RealtimeAudioFrame; use std::sync::Arc; use std::sync::Mutex; @@ -144,7 +149,7 @@ mod voice { Err("voice input is unavailable in this build".to_string()) } - pub fn start_realtime(_tx: AppEventSender) -> Result { + pub fn start_realtime(_config: &Config, _tx: AppEventSender) -> Result { Err("voice input is unavailable in this build".to_string()) } @@ -184,7 +189,7 @@ mod voice { } impl RealtimeAudioPlayer { - pub(crate) fn start() -> Result { + pub(crate) fn start(_config: &Config) -> Result { Err("voice output is unavailable in this build".to_string()) } @@ -216,15 +221,13 @@ use crate::onboarding::onboarding_screen::OnboardingScreenArgs; use crate::onboarding::onboarding_screen::run_onboarding_app; use crate::tui::Tui; pub use cli::Cli; +use codex_arg0::Arg0DispatchPaths; pub use markdown_render::render_markdown_text; pub use public_widgets::composer_input::ComposerAction; pub use public_widgets::composer_input::ComposerInput; // (tests access modules directly within the crate) -pub async fn run_main( - mut cli: Cli, - codex_linux_sandbox_exe: Option, -) -> std::io::Result { +pub async fn run_main(mut cli: Cli, arg0_paths: Arg0DispatchPaths) -> std::io::Result { let (sandbox_mode, approval_policy) = if cli.full_auto { ( Some(SandboxMode::WorkspaceWrite), @@ -373,7 +376,8 @@ pub async fn run_main( cwd, model_provider: model_provider_override.clone(), config_profile: cli.config_profile.clone(), - codex_linux_sandbox_exe, + codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(), + main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(), show_raw_agent_reasoning: cli.oss.then_some(true), additional_writable_roots: additional_dirs, ..Default::default() @@ -666,7 +670,19 @@ async fn run_ratatui_app( find_thread_path_by_name_str(&config.codex_home, id_str).await? }; match path { - Some(path) => resume_picker::SessionSelection::Fork(path), + Some(path) => { + let thread_id = + match resolve_session_thread_id(path.as_path(), is_uuid.then_some(id_str)) + .await + { + Some(thread_id) => thread_id, + None => return missing_session_exit(id_str, "fork"), + }; + resume_picker::SessionSelection::Fork(resume_picker::SessionTarget { + path, + thread_id, + }) + } None => return missing_session_exit(id_str, "fork"), } } else if cli.fork_last { @@ -679,14 +695,41 @@ async fn run_ratatui_app( INTERACTIVE_SESSION_SOURCES, Some(provider_filter.as_slice()), &config.model_provider_id, + None, ) .await { - Ok(page) => page - .items - .first() - .map(|it| resume_picker::SessionSelection::Fork(it.path.clone())) - .unwrap_or(resume_picker::SessionSelection::StartFresh), + Ok(page) => match page.items.first() { + Some(item) => { + match resolve_session_thread_id(item.path.as_path(), None).await { + Some(thread_id) => resume_picker::SessionSelection::Fork( + resume_picker::SessionTarget { + path: item.path.clone(), + thread_id, + }, + ), + None => { + let rollout_path = item.path.display(); + error!( + "Error reading session metadata from latest rollout: {rollout_path}" + ); + restore(); + session_log::log_session_end(); + let _ = tui.terminal.clear(); + return Ok(AppExitInfo { + token_usage: codex_protocol::protocol::TokenUsage::default(), + thread_id: None, + thread_name: None, + update_action: None, + exit_reason: ExitReason::Fatal(format!( + "Found latest saved session at {rollout_path}, but failed to read its metadata. Run `codex fork` to choose from existing sessions." + )), + }); + } + } + } + None => resume_picker::SessionSelection::StartFresh, + }, Err(_) => resume_picker::SessionSelection::StartFresh, } } else if cli.fork_picker { @@ -715,7 +758,21 @@ async fn run_ratatui_app( find_thread_path_by_name_str(&config.codex_home, id_str).await? }; match path { - Some(path) => resume_picker::SessionSelection::Resume(path), + Some(path) => { + let thread_id = match resolve_session_thread_id( + path.as_path(), + is_uuid.then_some(id_str), + ) + .await + { + Some(thread_id) => thread_id, + None => return missing_session_exit(id_str, "resume"), + }; + resume_picker::SessionSelection::Resume(resume_picker::SessionTarget { + path, + thread_id, + }) + } None => return missing_session_exit(id_str, "resume"), } } else if cli.resume_last { @@ -737,7 +794,30 @@ async fn run_ratatui_app( ) .await { - Ok(Some(path)) => resume_picker::SessionSelection::Resume(path), + Ok(Some(path)) => match resolve_session_thread_id(path.as_path(), None).await { + Some(thread_id) => { + resume_picker::SessionSelection::Resume(resume_picker::SessionTarget { + path, + thread_id, + }) + } + None => { + let rollout_path = path.display(); + error!("Error reading session metadata from latest rollout: {rollout_path}"); + restore(); + session_log::log_session_end(); + let _ = tui.terminal.clear(); + return Ok(AppExitInfo { + token_usage: codex_protocol::protocol::TokenUsage::default(), + thread_id: None, + thread_name: None, + update_action: None, + exit_reason: ExitReason::Fatal(format!( + "Found latest saved session at {rollout_path}, but failed to read its metadata. Run `codex resume` to choose from existing sessions." + )), + }); + } + }, _ => resume_picker::SessionSelection::StartFresh, } } else if cli.resume_picker { @@ -761,15 +841,27 @@ async fn run_ratatui_app( let current_cwd = config.cwd.clone(); let allow_prompt = cli.cwd.is_none(); - let action_and_path_if_resume_or_fork = match &session_selection { - resume_picker::SessionSelection::Resume(path) => Some((CwdPromptAction::Resume, path)), - resume_picker::SessionSelection::Fork(path) => Some((CwdPromptAction::Fork, path)), + let action_and_target_session_if_resume_or_fork = match &session_selection { + resume_picker::SessionSelection::Resume(target_session) => { + Some((CwdPromptAction::Resume, target_session)) + } + resume_picker::SessionSelection::Fork(target_session) => { + Some((CwdPromptAction::Fork, target_session)) + } _ => None, }; - let fallback_cwd = match action_and_path_if_resume_or_fork { - Some((action, path)) => { - match resolve_cwd_for_resume_or_fork(&mut tui, ¤t_cwd, path, action, allow_prompt) - .await? + let fallback_cwd = match action_and_target_session_if_resume_or_fork { + Some((action, target_session)) => { + match resolve_cwd_for_resume_or_fork( + &mut tui, + &config, + ¤t_cwd, + target_session.thread_id, + &target_session.path, + action, + allow_prompt, + ) + .await? { ResolveCwdOutcome::Continue(cwd) => cwd, ResolveCwdOutcome::Exit => { @@ -851,12 +943,35 @@ async fn run_ratatui_app( app_result } -pub(crate) async fn read_session_cwd(path: &Path) -> Option { +pub(crate) async fn resolve_session_thread_id( + path: &Path, + id_str_if_uuid: Option<&str>, +) -> Option { + match id_str_if_uuid { + Some(id_str) => ThreadId::from_string(id_str).ok(), + None => read_session_meta_line(path) + .await + .ok() + .map(|meta_line| meta_line.meta.id), + } +} + +pub(crate) async fn read_session_cwd( + config: &Config, + thread_id: ThreadId, + path: &Path, +) -> Option { + if let Some(state_db_ctx) = get_state_db(config, None).await + && let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await + { + return Some(metadata.cwd); + } + // Prefer the latest TurnContext cwd so resume/fork reflects the most recent - // session directory (for the changed-cwd prompt). The alternative would be - // mutating the SessionMeta line when the session cwd changes, but the rollout - // is an append-only JSONL log and rewriting the head would be error-prone. - // When rollouts move to SQLite, we can drop this scan. + // session directory (for the changed-cwd prompt) when DB data is unavailable. + // The alternative would be mutating the SessionMeta line when the session cwd + // changes, but the rollout is an append-only JSONL log and rewriting the head + // would be error-prone. if let Some(cwd) = parse_latest_turn_context_cwd(path).await { return Some(cwd); } @@ -908,12 +1023,14 @@ pub(crate) enum ResolveCwdOutcome { pub(crate) async fn resolve_cwd_for_resume_or_fork( tui: &mut Tui, + config: &Config, current_cwd: &Path, + thread_id: ThreadId, path: &Path, action: CwdPromptAction, allow_prompt: bool, ) -> color_eyre::Result { - let Some(history_cwd) = read_session_cwd(path).await else { + let Some(history_cwd) = read_session_cwd(config, thread_id, path).await else { return Ok(ResolveCwdOutcome::Continue(None)); }; if allow_prompt && cwds_differ(current_cwd, &history_cwd) { @@ -1071,11 +1188,14 @@ mod tests { use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::ProjectConfig; + use codex_core::features::Feature; + use codex_protocol::ThreadId; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; + use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TurnContextItem; use serial_test::serial; use tempfile::TempDir; @@ -1152,6 +1272,8 @@ mod tests { TurnContextItem { turn_id: None, cwd, + current_date: None, + timezone: None, approval_policy: config.permissions.approval_policy.value(), sandbox_policy: config.permissions.sandbox_policy.get().clone(), network: None, @@ -1159,7 +1281,9 @@ mod tests { personality: None, collaboration_mode: None, effort: config.model_reasoning_effort, - summary: config.model_reasoning_summary, + summary: config + .model_reasoning_summary + .unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto), user_instructions: None, developer_instructions: None, final_output_json_schema: None, @@ -1194,7 +1318,9 @@ mod tests { } std::fs::write(&rollout_path, text)?; - let cwd = read_session_cwd(&rollout_path).await.expect("expected cwd"); + let cwd = read_session_cwd(&config, ThreadId::new(), &rollout_path) + .await + .expect("expected cwd"); assert_eq!(cwd, second); Ok(()) } @@ -1234,7 +1360,9 @@ mod tests { } std::fs::write(&rollout_path, text)?; - let session_cwd = read_session_cwd(&rollout_path).await.expect("expected cwd"); + let session_cwd = read_session_cwd(&config, ThreadId::new(), &rollout_path) + .await + .expect("expected cwd"); assert_eq!(session_cwd, latest); assert!(cwds_differ(¤t, &session_cwd)); Ok(()) @@ -1339,7 +1467,7 @@ trust_level = "untrusted" #[tokio::test] async fn read_session_cwd_falls_back_to_session_meta() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let _config = build_config(&temp_dir).await?; + let config = build_config(&temp_dir).await?; let session_cwd = temp_dir.path().join("session"); std::fs::create_dir_all(&session_cwd)?; @@ -1361,8 +1489,67 @@ trust_level = "untrusted" ); std::fs::write(&rollout_path, text)?; - let cwd = read_session_cwd(&rollout_path).await.expect("expected cwd"); + let cwd = read_session_cwd(&config, ThreadId::new(), &rollout_path) + .await + .expect("expected cwd"); assert_eq!(cwd, session_cwd); Ok(()) } + + #[tokio::test] + async fn read_session_cwd_prefers_sqlite_when_thread_id_present() -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let mut config = build_config(&temp_dir).await?; + config.features.enable(Feature::Sqlite); + + let thread_id = ThreadId::new(); + let rollout_cwd = temp_dir.path().join("rollout-cwd"); + let sqlite_cwd = temp_dir.path().join("sqlite-cwd"); + std::fs::create_dir_all(&rollout_cwd)?; + std::fs::create_dir_all(&sqlite_cwd)?; + + let rollout_path = temp_dir.path().join("rollout.jsonl"); + let rollout_line = RolloutLine { + timestamp: "t0".to_string(), + item: RolloutItem::TurnContext(build_turn_context(&config, rollout_cwd)), + }; + std::fs::write( + &rollout_path, + format!( + "{}\n", + serde_json::to_string(&rollout_line).expect("serialize rollout") + ), + )?; + + let runtime = codex_state::StateRuntime::init( + config.codex_home.clone(), + config.model_provider_id.clone(), + None, + ) + .await + .map_err(std::io::Error::other)?; + runtime + .mark_backfill_complete(None) + .await + .map_err(std::io::Error::other)?; + + let mut builder = codex_state::ThreadMetadataBuilder::new( + thread_id, + rollout_path.clone(), + chrono::Utc::now(), + SessionSource::Cli, + ); + builder.cwd = sqlite_cwd.clone(); + let metadata = builder.build(config.model_provider_id.as_str()); + runtime + .upsert_thread(&metadata) + .await + .map_err(std::io::Error::other)?; + + let cwd = read_session_cwd(&config, thread_id, &rollout_path) + .await + .expect("expected cwd"); + assert_eq!(cwd, sqlite_cwd); + Ok(()) + } } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index b7a27576e8..5ee9ce5a47 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use codex_arg0::Arg0DispatchPaths; use codex_arg0::arg0_dispatch_or_else; use codex_tui::Cli; use codex_tui::run_main; @@ -14,14 +15,14 @@ struct TopCli { } fn main() -> anyhow::Result<()> { - arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { let top_cli = TopCli::parse(); let mut inner = top_cli.inner; inner .config_overrides .raw_overrides .splice(0..0, top_cli.config_overrides.raw_overrides); - let exit_info = run_main(inner, codex_linux_sandbox_exe).await?; + let exit_info = run_main(inner, arg0_paths).await?; let token_usage = exit_info.token_usage; if !token_usage.is_zero() { println!( diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index ba8b47d5ed..2bbe19b6f7 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -2,6 +2,7 @@ use crate::render::highlight::highlight_code_to_lines; use crate::render::line_utils::line_to_static; use crate::wrapping::RtOptions; use crate::wrapping::adaptive_wrap_line; +use codex_utils_string::normalize_markdown_hash_location_suffix; use pulldown_cmark::CodeBlockKind; use pulldown_cmark::CowStr; use pulldown_cmark::Event; @@ -14,6 +15,8 @@ use ratatui::style::Style; use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; +use regex_lite::Regex; +use std::sync::LazyLock; struct MarkdownStyles { h1: Style, @@ -85,6 +88,48 @@ pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) w.text } +#[derive(Clone, Debug)] +struct LinkState { + destination: String, + show_destination: bool, + hidden_location_suffix: Option, + label_start_span_idx: usize, + label_styled: bool, +} + +fn should_render_link_destination(dest_url: &str) -> bool { + !is_local_path_like_link(dest_url) +} + +static COLON_LOCATION_SUFFIX_RE: LazyLock = + LazyLock::new( + || match Regex::new(r":\d+(?::\d+)?(?:[-–]\d+(?::\d+)?)?$") { + Ok(regex) => regex, + Err(error) => panic!("invalid location suffix regex: {error}"), + }, + ); + +// Covered by load_location_suffix_regexes. +static HASH_LOCATION_SUFFIX_RE: LazyLock = + LazyLock::new(|| match Regex::new(r"^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$") { + Ok(regex) => regex, + Err(error) => panic!("invalid hash location regex: {error}"), + }); + +fn is_local_path_like_link(dest_url: &str) -> bool { + dest_url.starts_with("file://") + || dest_url.starts_with('/') + || dest_url.starts_with("~/") + || dest_url.starts_with("./") + || dest_url.starts_with("../") + || dest_url.starts_with("\\\\") + || matches!( + dest_url.as_bytes(), + [drive, b':', separator, ..] + if drive.is_ascii_alphabetic() && matches!(separator, b'/' | b'\\') + ) +} + struct Writer<'a, I> where I: Iterator>, @@ -95,7 +140,7 @@ where inline_styles: Vec