diff --git a/.bazelrc b/.bazelrc index 2e3d53b620..764754c216 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,6 +4,7 @@ common --repo_env=BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 common --disk_cache=~/.cache/bazel-disk-cache common --repo_contents_cache=~/.cache/bazel-repo-contents-cache common --repository_cache=~/.cache/bazel-repo-cache +common --remote_cache_compression startup --experimental_remote_repo_contents_cache common --experimental_platform_in_output_dir diff --git a/.codespellrc b/.codespellrc index 84b4495e31..a3f0cd501a 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file -skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt +skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new check-hidden = true ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-words-list = ratatui,ser,iTerm,iterm2,iterm diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index a8efec1089..7a2ef1f2df 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -59,7 +59,7 @@ jobs: working-directory: codex-rs steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@1.92 + - uses: dtolnay/rust-toolchain@1.93 with: components: rustfmt - name: cargo fmt @@ -77,7 +77,7 @@ jobs: working-directory: codex-rs steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@1.92 + - uses: dtolnay/rust-toolchain@1.93 - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2 with: tool: cargo-shear @@ -177,11 +177,31 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@1.92 + - name: Install UBSan runtime (musl) + if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} components: clippy + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Use hermetic Cargo home (musl) + shell: bash + run: | + set -euo pipefail + cargo_home="${GITHUB_WORKSPACE}/.cargo-home" + mkdir -p "${cargo_home}/bin" + echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV" + echo "${cargo_home}/bin" >> "$GITHUB_PATH" + : > "${cargo_home}/config.toml" + - name: Compute lockfile hash id: lockhash working-directory: codex-rs @@ -202,6 +222,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }} restore-keys: | cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}- @@ -244,6 +268,14 @@ jobs: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}- sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}- + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Disable sccache wrapper (musl) + shell: bash + run: | + set -euo pipefail + echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Prepare APT cache directories (musl) shell: bash @@ -277,6 +309,58 @@ jobs: shell: bash run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + - name: Install cargo-chef if: ${{ matrix.profile == 'release' }} uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2 @@ -322,6 +406,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }} - name: Save sccache cache (fallback) @@ -422,7 +510,7 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@v2 - - uses: dtolnay/rust-toolchain@1.92 + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 35b91e25d5..2f764df680 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -21,7 +21,6 @@ jobs: steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@1.92 - - name: Validate tag matches Cargo.toml version shell: bash run: | @@ -90,10 +89,30 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@1.92 + - name: Install UBSan runtime (musl) + if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Use hermetic Cargo home (musl) + shell: bash + run: | + set -euo pipefail + cargo_home="${GITHUB_WORKSPACE}/.cargo-home" + mkdir -p "${cargo_home}/bin" + echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV" + echo "${cargo_home}/bin" >> "$GITHUB_PATH" + : > "${cargo_home}/config.toml" + - uses: actions/cache@v5 with: path: | @@ -101,6 +120,10 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ + ${{ github.workspace }}/.cargo-home/bin/ + ${{ github.workspace }}/.cargo-home/registry/index/ + ${{ github.workspace }}/.cargo-home/registry/cache/ + ${{ github.workspace }}/.cargo-home/git/db/ ${{ github.workspace }}/codex-rs/target/ key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }} @@ -116,6 +139,58 @@ jobs: TARGET: ${{ matrix.target }} run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + - name: Cargo build shell: bash run: | @@ -252,6 +327,7 @@ jobs: # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" + repo_root=$PWD # We want to ship the raw Windows executables in the GitHub Release # in addition to the compressed archives. Keep the originals for @@ -305,7 +381,7 @@ jobs: cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" # Use an absolute path so bundle zips land in the real dist # dir even when 7z runs from a temp directory. - (cd "$bundle_dir" && 7z a "$(pwd)/$dest/${base}.zip" .) + (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) else echo "warning: missing sandbox binaries; falling back to single-binary zip" echo "warning: expected $runner_src and $setup_src" diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index f506dce6ba..60c14561ab 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -24,7 +24,7 @@ jobs: node-version: 22 cache: pnpm - - uses: dtolnay/rust-toolchain@1.92 + - uses: dtolnay/rust-toolchain@1.93 - name: build codex run: cargo build --bin codex diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml index a8a01b5f6a..88cdc28b3a 100644 --- a/.github/workflows/shell-tool-mcp.yml +++ b/.github/workflows/shell-tool-mcp.yml @@ -93,7 +93,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@1.92 + - name: Install UBSan runtime (musl) + if: ${{ matrix.install_musl }} + shell: bash + run: | + set -euo pipefail + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 + fi + + - uses: dtolnay/rust-toolchain@1.93 with: targets: ${{ matrix.target }} @@ -109,6 +119,58 @@ jobs: TARGET: ${{ matrix.target }} run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh" + - if: ${{ matrix.install_musl }} + name: Configure rustc UBSan wrapper (musl host) + shell: bash + run: | + set -euo pipefail + ubsan="" + if command -v ldconfig >/dev/null 2>&1; then + ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" + fi + wrapper_root="${RUNNER_TEMP:-/tmp}" + wrapper="${wrapper_root}/rustc-ubsan-wrapper" + cat > "${wrapper}" <> "$GITHUB_ENV" + echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV" + + - if: ${{ matrix.install_musl }} + name: Clear sanitizer flags (musl) + shell: bash + run: | + set -euo pipefail + # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. + echo "RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV" + # Override any runner-level Cargo config rustflags as well. + echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV" + + sanitize_flags() { + local input="$1" + input="${input//-fsanitize=undefined/}" + input="${input//-fno-sanitize-recover=undefined/}" + input="${input//-fno-sanitize-trap=undefined/}" + echo "$input" + } + + cflags="$(sanitize_flags "${CFLAGS-}")" + cxxflags="$(sanitize_flags "${CXXFLAGS-}")" + echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" + echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + - name: Build exec server binaries run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper @@ -282,7 +344,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.8.1 run_install: false - name: Setup Node.js @@ -378,7 +439,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.8.1 run_install: false - name: Setup Node.js diff --git a/MODULE.bazel b/MODULE.bazel index 87db7d1522..df0df9b25c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -53,7 +53,7 @@ rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") rust.toolchain( edition = "2024", extra_target_triples = RUST_TRIPLES, - versions = ["1.90.0"], + versions = ["1.93.0"], ) use_repo(rust, "rust_toolchains") @@ -67,6 +67,11 @@ crate.from_cargo( cargo_toml = "//codex-rs:Cargo.toml", platform_triples = RUST_TRIPLES, ) +crate.annotation( + crate = "nucleo-matcher", + strip_prefix = "matcher", + version = "0.3.1", +) bazel_dep(name = "openssl", version = "3.5.4.bcr.0") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index f0bb43940d..7444d3fdbe 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -595,14 +595,17 @@ "async-stream_0.3.6": "{\"dependencies\":[{\"name\":\"async-stream-impl\",\"req\":\"=0.3.6\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{}}", "async-task_4.7.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"atomic-waker\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"flaky_test\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"flume\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"smol\",\"req\":\"^2\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "async-trait_0.1.89": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.30\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"full\",\"parsing\",\"printing\",\"proc-macro\",\"visit-mut\"],\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.40\"},{\"kind\":\"dev\",\"name\":\"tracing-attributes\",\"req\":\"^0.1.27\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.81\"}],\"features\":{}}", + "asynk-strim_0.1.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-fn-stream\",\"req\":\"^0.3.2\"},{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\",\"plotters\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.3.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.99\"}],\"features\":{}}", + "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\":{}}", - "axum-core_0.5.2": "{\"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\":\"rustversion\",\"req\":\"^1.0.9\"},{\"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.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.2\"},{\"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\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.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.26.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.26.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: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\"]}}", + "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\"]}}", "backtrace_0.3.75": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.24.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.36.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.7.3\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-targets\",\"req\":\"^0.52.6\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", "base64_0.22.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64ct_1.8.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.6\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "beef_0.5.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.105\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.105\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"const_fn\":[],\"default\":[],\"impl_serde\":[\"serde\"]}}", + "bindgen_0.72.1": "{\"dependencies\":[{\"name\":\"annotate-snippets\",\"optional\":true,\"req\":\"^0.11.4\"},{\"name\":\"bitflags\",\"req\":\"^2.2.1\"},{\"name\":\"cexpr\",\"req\":\"^0.6\"},{\"features\":[\"clang_11_0\"],\"name\":\"clang-sys\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4\"},{\"name\":\"clap_complete\",\"optional\":true,\"req\":\"^4\"},{\"default_features\":false,\"name\":\"itertools\",\"req\":\">=0.10, <0.14\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"verbatim\"],\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.7\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-perl\"],\"name\":\"regex\",\"req\":\"^1.5.3\"},{\"name\":\"rustc-hash\",\"req\":\"^2.1.0\"},{\"name\":\"shlex\",\"req\":\"^1\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{\"__cli\":[\"dep:clap\",\"dep:clap_complete\"],\"__testing_only_extra_assertions\":[],\"__testing_only_libclang_16\":[],\"__testing_only_libclang_9\":[],\"default\":[\"logging\",\"prettyplease\",\"runtime\"],\"experimental\":[\"dep:annotate-snippets\"],\"logging\":[\"dep:log\"],\"runtime\":[\"clang-sys/runtime\"],\"static\":[\"clang-sys/static\"]}}", "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\"]}}", @@ -610,6 +613,7 @@ "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\":[]}}", "blocking_1.6.2": "{\"dependencies\":[{\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"name\":\"async-task\",\"req\":\"^4.4.0\"},{\"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\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"piper\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{}}", + "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.0": "{\"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.19.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.8\"},{\"kind\":\"dev\",\"name\":\"blink-alloc\",\"req\":\"=0.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"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.23.1": "{\"dependencies\":[{\"name\":\"bytemuck_derive\",\"optional\":true,\"req\":\"^1.4.1\"}],\"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\":[],\"nightly_stdsimd\":[],\"pod_saturating\":[],\"track_caller\":[],\"transparentwrapper_extra\":[],\"unsound_ptr_pod_impl\":[],\"wasm_simd\":[],\"zeroable_atomics\":[],\"zeroable_maybe_uninit\":[],\"zeroable_unwind_fn\":[]}}", @@ -620,8 +624,9 @@ "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.30": "{\"dependencies\":[{\"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.52": "{\"dependencies\":[{\"name\":\"find-msvc-tools\",\"req\":\"^0.1.7\"},{\"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.1": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"rustc-dep-of-std\":[\"core\"]}}", "cfg_aliases_0.1.1": "{\"dependencies\":[],\"features\":{}}", "cfg_aliases_0.2.1": "{\"dependencies\":[],\"features\":{}}", @@ -629,12 +634,14 @@ "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.54": "{\"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.54\"},{\"name\":\"clap_derive\",\"optional\":true,\"req\":\"=4.5.49\"},{\"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.54": "{\"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\":\"^0.7.4\"},{\"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.64": "{\"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.49": "{\"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_0.7.5": "{\"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\"]}}", + "cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}", "cmp_any_0.8.1": "{\"dependencies\":[],\"features\":{}}", "color-eyre_0.6.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ansi-parser\",\"req\":\"^0.8.0\"},{\"name\":\"backtrace\",\"req\":\"^0.3.59\"},{\"name\":\"color-spantrace\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"eyre\",\"req\":\"^0.6\"},{\"name\":\"indenter\",\"req\":\"^0.3.0\"},{\"name\":\"once_cell\",\"req\":\"^1.18.0\"},{\"name\":\"owo-colors\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.13\"},{\"name\":\"tracing-error\",\"optional\":true,\"req\":\"^0.2.0\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.1.1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.15\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"capture-spantrace\":[\"tracing-error\",\"color-spantrace\"],\"default\":[\"track-caller\",\"capture-spantrace\"],\"issue-url\":[\"url\"],\"track-caller\":[]}}", "color-spantrace_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ansi-parser\",\"req\":\"^0.8\"},{\"name\":\"once_cell\",\"req\":\"^1.18.0\"},{\"name\":\"owo-colors\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.29\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.21\"},{\"name\":\"tracing-error\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.4\"}],\"features\":{}}", @@ -644,20 +651,29 @@ "concurrent-queue_2.5.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.11\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "console_0.15.11": "{\"dependencies\":[{\"name\":\"encode_unicode\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2.99\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"default_features\":false,\"features\":[\"std\",\"bit-set\",\"break-dead-code\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.4.2\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Storage_FileSystem\",\"Win32_UI_Input_KeyboardAndMouse\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"ansi-parsing\":[],\"default\":[\"unicode-width\",\"ansi-parsing\"],\"windows-console-colors\":[\"ansi-parsing\"]}}", "const-hex_1.17.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"kind\":\"dev\",\"name\":\"divan\",\"package\":\"codspeed-divan-compat\",\"req\":\"^3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"faster-hex\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"~0.4.2\"},{\"default_features\":false,\"name\":\"proptest\",\"optional\":true,\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"rustc-hex\",\"req\":\"^2.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"__fuzzing\":[\"dep:proptest\",\"std\"],\"alloc\":[\"serde_core?/alloc\",\"proptest?/alloc\"],\"core-error\":[],\"default\":[\"std\"],\"force-generic\":[],\"hex\":[],\"nightly\":[],\"portable-simd\":[],\"serde\":[\"dep:serde_core\"],\"std\":[\"serde_core?/std\",\"proptest?/std\",\"alloc\"]}}", + "const-oid_0.9.6": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"}],\"features\":{\"db\":[],\"std\":[]}}", + "const_format_0.2.35": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.7.0\"},{\"name\":\"const_format_proc_macros\",\"req\":\"=0.2.34\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.5\"},{\"default_features\":false,\"name\":\"konst\",\"optional\":true,\"req\":\"^0.2.13\"}],\"features\":{\"__debug\":[\"const_format_proc_macros/debug\"],\"__docsrs\":[],\"__inline_const_pat_tests\":[\"__test\",\"fmt\"],\"__only_new_tests\":[\"__test\"],\"__test\":[],\"all\":[\"fmt\",\"derive\",\"rust_1_64\",\"assert\"],\"assert\":[\"assertc\"],\"assertc\":[\"fmt\",\"assertcp\"],\"assertcp\":[\"rust_1_51\"],\"const_generics\":[\"rust_1_51\"],\"constant_time_as_str\":[\"fmt\"],\"default\":[],\"derive\":[\"fmt\",\"const_format_proc_macros/derive\"],\"fmt\":[\"rust_1_83\"],\"more_str_macros\":[\"rust_1_64\"],\"nightly_const_generics\":[\"const_generics\"],\"rust_1_51\":[],\"rust_1_64\":[\"rust_1_51\",\"konst\",\"konst/rust_1_64\"],\"rust_1_83\":[\"rust_1_64\"]}}", + "const_format_proc_macros_0.2.34": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.19\"},{\"name\":\"quote\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.38\"},{\"name\":\"unicode-xid\",\"req\":\"^0.2\"}],\"features\":{\"all\":[\"derive\"],\"debug\":[\"syn/extra-traits\"],\"default\":[],\"derive\":[\"syn\",\"syn/derive\",\"syn/printing\"]}}", "convert_case_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{}}", "convert_case_0.6.0": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.18.0\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{\"random\":[\"rand\"]}}", "core-foundation-sys_0.8.7": "{\"dependencies\":[],\"features\":{\"default\":[\"link\"],\"link\":[],\"mac_os_10_7_support\":[],\"mac_os_10_8_features\":[]}}", "core-foundation_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-uuid\":[\"dep:uuid\"]}}", "core-foundation_0.9.4": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-chrono\":[\"chrono\"],\"with-uuid\":[\"uuid\"]}}", "cpufeatures_0.2.17": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"aarch64-linux-android\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"linux\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"loongarch64\\\", target_os = \\\"linux\\\"))\"}],\"features\":{}}", + "crc-catalog_2.4.0": "{\"dependencies\":[],\"features\":{}}", "crc32fast_1.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", + "crc_3.4.0": "{\"dependencies\":[{\"name\":\"crc-catalog\",\"req\":\"^2.4.0\"}],\"features\":{}}", + "critical-section_1.2.0": "{\"dependencies\":[],\"features\":{\"restore-state-bool\":[],\"restore-state-none\":[],\"restore-state-u16\":[],\"restore-state-u32\":[],\"restore-state-u64\":[],\"restore-state-u8\":[],\"restore-state-usize\":[],\"std\":[\"restore-state-bool\"]}}", "crossbeam-channel_0.5.15": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"crossbeam-utils/std\"]}}", "crossbeam-deque_0.8.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-epoch\",\"req\":\"^0.9.17\"},{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"crossbeam-epoch/std\",\"crossbeam-utils/std\"]}}", "crossbeam-epoch_0.9.18": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"name\":\"loom-crate\",\"optional\":true,\"package\":\"loom\",\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"loom\":[\"loom-crate\",\"crossbeam-utils/loom\"],\"nightly\":[\"crossbeam-utils/nightly\"],\"std\":[\"alloc\",\"crossbeam-utils/std\"]}}", + "crossbeam-queue_0.3.12": "{\"dependencies\":[{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.18\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"nightly\":[\"crossbeam-utils/nightly\"],\"std\":[\"alloc\",\"crossbeam-utils/std\"]}}", "crossbeam-utils_0.8.21": "{\"dependencies\":[{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", "crossterm_winapi_0.9.1": "{\"dependencies\":[{\"features\":[\"winbase\",\"consoleapi\",\"processenv\",\"handleapi\",\"synchapi\",\"impl-default\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", "crypto-common_0.1.6": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"^0.14.4\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", + "csv-core_0.1.13": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2\"}],\"features\":{\"default\":[],\"libc\":[\"memchr/libc\"]}}", + "csv_1.4.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"serde\"],\"kind\":\"dev\",\"name\":\"bstr\",\"req\":\"^1.7.0\"},{\"name\":\"csv-core\",\"req\":\"^0.1.11\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"ryu\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"}],\"features\":{}}", "ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}", "ctor_0.1.26": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^1.0.98\"}],\"features\":{}}", "ctor_0.6.3": "{\"dependencies\":[{\"name\":\"ctor-proc-macro\",\"optional\":true,\"req\":\"=0.0.7\"},{\"default_features\":false,\"name\":\"dtor\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"}],\"features\":{\"__no_warn_on_missing_unsafe\":[\"dtor?/__no_warn_on_missing_unsafe\"],\"default\":[\"dtor\",\"proc_macro\",\"__no_warn_on_missing_unsafe\"],\"dtor\":[\"dep:dtor\"],\"proc_macro\":[\"dep:ctor-proc-macro\",\"dtor?/proc_macro\"],\"used_linker\":[\"dtor?/used_linker\"]}}", @@ -710,6 +726,8 @@ "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.0": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "endian-type_0.1.2": "{\"dependencies\":[],\"features\":{}}", + "endian-type_0.2.0": "{\"dependencies\":[],\"features\":{}}", + "enum-as-inner_0.6.1": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "enumflags2_0.7.12": "{\"dependencies\":[{\"name\":\"enumflags2_derive\",\"req\":\"=0.7.12\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.0\"}],\"features\":{\"std\":[]}}", "enumflags2_derive_0.7.12": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"derive\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "env-flags_0.1.1": "{\"dependencies\":[],\"features\":{}}", @@ -720,6 +738,7 @@ "erased-serde_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"serde\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_cbor\",\"req\":\"^0.11.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.99\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.83\"}],\"features\":{\"alloc\":[\"serde/alloc\"],\"default\":[\"std\"],\"std\":[\"serde/std\"],\"unstable-debug\":[]}}", "errno_0.3.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os=\\\"hermit\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os=\\\"wasi\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Diagnostics_Debug\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"libc/std\"]}}", "error-code_3.3.2": "{\"dependencies\":[],\"features\":{\"std\":[]}}", + "etcetera_0.8.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"home\",\"req\":\"^0.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\"^0.48\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "event-listener-strategy_0.5.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"event-listener\",\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.12\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"event-listener/loom\"],\"portable-atomic\":[\"event-listener/portable-atomic\"],\"std\":[\"event-listener/std\"]}}", "event-listener_5.4.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"concurrent-queue\",\"req\":\"^2.4.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.2.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"critical-section\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.0.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.12\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"try-lock\",\"req\":\"^0.2.5\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"concurrent-queue/loom\",\"parking?/loom\",\"dep:loom\"],\"portable-atomic\":[\"portable-atomic-util\",\"portable_atomic_crate\",\"concurrent-queue/portable-atomic\"],\"std\":[\"concurrent-queue/std\",\"parking\"]}}", "eventsource-stream_0.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"nom\",\"req\":\"^7.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.8\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.11\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"nom/std\"]}}", @@ -730,21 +749,30 @@ "fd-lock_4.0.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.0.8\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "fdeflate_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"simd-adler32\",\"req\":\"^0.3.4\"}],\"features\":{}}", "filedescriptor_0.8.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"winuser\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"processthreadsapi\",\"winsock2\",\"processenv\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", + "find-msvc-tools_0.1.7": "{\"dependencies\":[],\"features\":{}}", "findshlibs_0.10.2": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.67\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"name\":\"libc\",\"req\":\"^0.2.104\"},{\"features\":[\"psapi\",\"memoryapi\",\"libloaderapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{}}", "fixed_decimal_0.7.0": "{\"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\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_distr\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3\"},{\"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,\"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\":[]}}", "flate2_1.1.2": "{\"dependencies\":[{\"name\":\"cloudflare-zlib-sys\",\"optional\":true,\"req\":\"^0.3.5\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"name\":\"libz-ng-sys\",\"optional\":true,\"req\":\"^1.1.16\"},{\"default_features\":false,\"features\":[\"std\",\"rust-allocator\"],\"name\":\"libz-rs-sys\",\"optional\":true,\"req\":\"^0.5.1\"},{\"default_features\":false,\"name\":\"libz-sys\",\"optional\":true,\"req\":\"^1.1.20\"},{\"default_features\":false,\"features\":[\"with-alloc\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8.5\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"emscripten\\\")))\"},{\"default_features\":false,\"features\":[\"with-alloc\"],\"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\"}],\"features\":{\"any_impl\":[],\"any_zlib\":[\"any_impl\"],\"cloudflare_zlib\":[\"any_zlib\",\"cloudflare-zlib-sys\"],\"default\":[\"rust_backend\"],\"miniz-sys\":[\"rust_backend\"],\"rust_backend\":[\"miniz_oxide\",\"any_impl\"],\"zlib\":[\"any_zlib\",\"libz-sys\"],\"zlib-default\":[\"any_zlib\",\"libz-sys/default\"],\"zlib-ng\":[\"any_zlib\",\"libz-ng-sys\"],\"zlib-ng-compat\":[\"zlib\",\"libz-sys/zlib-ng\"],\"zlib-rs\":[\"any_zlib\",\"libz-rs-sys\"]}}", "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\":[]}}", + "flume_0.11.1": "{\"dependencies\":[{\"features\":[\"attributes\",\"unstable\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5.5\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.15\"},{\"features\":[\"getrandom\"],\"name\":\"nanorand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"features\":[\"mutex\"],\"name\":\"spin1\",\"package\":\"spin\",\"req\":\"^0.9.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.16.1\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.1.0\"}],\"features\":{\"async\":[\"futures-sink\",\"futures-core\"],\"default\":[\"async\",\"select\",\"eventual-fairness\"],\"eventual-fairness\":[\"select\",\"nanorand\"],\"select\":[],\"spin\":[]}}", + "flume_0.12.0": "{\"dependencies\":[{\"features\":[\"attributes\",\"unstable\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5.5\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8.10\"},{\"features\":[\"std\",\"js\"],\"name\":\"fastrand\",\"optional\":true,\"req\":\"^2.3\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.15\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"features\":[\"mutex\"],\"name\":\"spin1\",\"package\":\"spin\",\"req\":\"^0.9.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.16.1\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.1.0\"}],\"features\":{\"async\":[\"futures-sink\",\"futures-core\"],\"default\":[\"async\",\"select\",\"eventual-fairness\"],\"eventual-fairness\":[\"select\",\"fastrand\"],\"select\":[],\"spin\":[]}}", "fnv_1.0.7": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "foldhash_0.1.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "foldhash_0.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rapidhash\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", + "foreign-types-macros_0.2.3": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{\"std\":[]}}", "foreign-types-shared_0.1.1": "{\"dependencies\":[],\"features\":{}}", + "foreign-types-shared_0.3.1": "{\"dependencies\":[],\"features\":{}}", "foreign-types_0.3.2": "{\"dependencies\":[{\"name\":\"foreign-types-shared\",\"req\":\"^0.1\"}],\"features\":{}}", + "foreign-types_0.5.0": "{\"dependencies\":[{\"name\":\"foreign-types-macros\",\"req\":\"^0.2\"},{\"name\":\"foreign-types-shared\",\"req\":\"^0.3\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"foreign-types-macros/std\"]}}", "form_urlencoded_1.2.1": "{\"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\":{}}", + "fslock_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.66\",\"target\":\"cfg(unix)\"},{\"features\":[\"minwindef\",\"minwinbase\",\"winbase\",\"errhandlingapi\",\"winerror\",\"winnt\",\"synchapi\",\"handleapi\",\"fileapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"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.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.31": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", @@ -753,6 +781,7 @@ "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\":[]}}", "gethostname_0.4.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.141\",\"target\":\"cfg(not(windows))\"},{\"name\":\"windows-targets\",\"req\":\"^0.48\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "getopts_0.2.23": "{\"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\",\"req\":\"^0.2.0\"}],\"features\":{\"rustc-dep-of-std\":[\"unicode-width/rustc-dep-of-std\",\"std\",\"core\"]}}", @@ -761,24 +790,31 @@ "gimli_0.31.1": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"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\",\"dep:compiler_builtins\"],\"std\":[\"fallible-iterator?/std\",\"stable_deref_trait?/std\"],\"write\":[\"dep:indexmap\"]}}", "git+https://github.com/JakkuSakura/tokio-tungstenite?rev=2ae536b0de793f3ddf31fc2f22d445bf1ef2023d#2ae536b0de793f3ddf31fc2f22d445bf1ef2023d_tokio-tungstenite": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"optional\":false},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"default_features\":false,\"features\":[],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"tokio-native-tls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tokio-rustls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tungstenite\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\",\"tokio-rustls\",\"stream\",\"tungstenite/__rustls-tls\",\"handshake\"],\"connect\":[\"stream\",\"tokio/net\",\"handshake\"],\"default\":[\"connect\",\"handshake\"],\"handshake\":[\"tungstenite/handshake\"],\"native-tls\":[\"native-tls-crate\",\"tokio-native-tls\",\"stream\",\"tungstenite/native-tls\",\"handshake\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\",\"tungstenite/native-tls-vendored\"],\"proxy\":[\"tungstenite/proxy\",\"tokio/net\",\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"stream\":[],\"url\":[\"tungstenite/url\"]},\"strip_prefix\":\"\"}", "git+https://github.com/JakkuSakura/tungstenite-rs?rev=f514de8644821113e5d18a027d6d28a5c8cc0a6e#f514de8644821113e5d18a027d6d28a5c8cc0a6e_tungstenite": "{\"dependencies\":[{\"name\":\"bytes\"},{\"default_features\":true,\"features\":[],\"name\":\"data-encoding\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"http\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"httparse\",\"optional\":true},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"name\":\"rand\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"sha1\",\"optional\":true},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"url\",\"optional\":true},{\"name\":\"utf-8\"},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"handshake\":[\"data-encoding\",\"http\",\"httparse\",\"sha1\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"proxy\":[\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]},\"strip_prefix\":\"\"}", + "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"nucleo-matcher\",\"optional\":false},{\"default_features\":true,\"features\":[\"send_guard\",\"arc_lock\"],\"name\":\"parking_lot\",\"optional\":false},{\"name\":\"rayon\"}],\"features\":{},\"strip_prefix\":\"\"}", + "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo-matcher": "{\"dependencies\":[{\"name\":\"memchr\"},{\"default_features\":true,\"features\":[],\"name\":\"unicode-segmentation\",\"optional\":true}],\"features\":{\"default\":[\"unicode-normalization\",\"unicode-casefold\",\"unicode-segmentation\"],\"unicode-casefold\":[],\"unicode-normalization\":[],\"unicode-segmentation\":[\"dep:unicode-segmentation\"]},\"strip_prefix\":\"matcher\"}", "git+https://github.com/nornagon/crossterm?branch=nornagon%2Fcolor-query#87db8bfa6dc99427fd3b071681b07fc31c6ce995_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true},{\"name\":\"parking_lot\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"windows\",\"events\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}", "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2_ratatui": "{\"dependencies\":[{\"name\":\"bitflags\"},{\"name\":\"cassowary\"},{\"name\":\"compact_str\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"document-features\",\"optional\":true},{\"name\":\"indoc\"},{\"name\":\"instability\"},{\"name\":\"itertools\"},{\"name\":\"lru\"},{\"default_features\":true,\"features\":[],\"name\":\"palette\",\"optional\":true},{\"name\":\"paste\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"strum\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"termwiz\",\"optional\":true},{\"default_features\":true,\"features\":[\"local-offset\"],\"name\":\"time\",\"optional\":true},{\"name\":\"unicode-segmentation\"},{\"name\":\"unicode-truncate\"},{\"name\":\"unicode-width\"},{\"default_features\":true,\"features\":[],\"name\":\"termion\",\"optional\":true,\"target\":\"cfg(not(windows))\"}],\"features\":{\"all-widgets\":[\"widget-calendar\"],\"crossterm\":[\"dep:crossterm\"],\"default\":[\"crossterm\",\"underline-color\"],\"macros\":[],\"palette\":[\"dep:palette\"],\"scrolling-regions\":[],\"serde\":[\"dep:serde\",\"bitflags/serde\",\"compact_str/serde\"],\"termion\":[\"dep:termion\"],\"termwiz\":[\"dep:termwiz\"],\"underline-color\":[\"dep:crossterm\"],\"unstable\":[\"unstable-rendered-line-info\",\"unstable-widget-ref\",\"unstable-backend-writer\"],\"unstable-backend-writer\":[],\"unstable-rendered-line-info\":[],\"unstable-widget-ref\":[],\"widget-calendar\":[\"dep:time\"]},\"strip_prefix\":\"\"}", - "globset_0.4.16": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}", + "glob_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"}],\"features\":{}}", + "globset_0.4.18": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}", "h2_0.4.11": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.26\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "half_2.6.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.4.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"crunchy\",\"req\":\"^0.2.2\",\"target\":\"cfg(target_arch = \\\"spirv\\\")\"},{\"kind\":\"dev\",\"name\":\"crunchy\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.16\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_distr\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8.23\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"rand_distr\":[\"dep:rand\",\"dep:rand_distr\"],\"std\":[\"alloc\"],\"use-intrinsics\":[]}}", "hashbrown_0.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"bumpalo\",\"optional\":true,\"req\":\"^3.5.0\"},{\"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\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"ahash-compile-time-rng\":[\"ahash/compile-time-rng\"],\"default\":[\"ahash\",\"inline-more\"],\"inline-more\":[],\"nightly\":[],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.14.5": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8.7\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"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\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.42\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7.42\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"ahash\",\"inline-more\",\"allocator-api2\"],\"inline-more\":[],\"nightly\":[\"allocator-api2?/nightly\",\"bumpalo/allocator_api\"],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.15.4": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"default-hasher\",\"inline-more\",\"allocator-api2\",\"equivalent\",\"raw-entry\"],\"default-hasher\":[\"dep:foldhash\"],\"inline-more\":[],\"nightly\":[\"bumpalo/allocator_api\"],\"raw-entry\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.16.0": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"default-hasher\",\"inline-more\",\"allocator-api2\",\"equivalent\",\"raw-entry\"],\"default-hasher\":[\"dep:foldhash\"],\"inline-more\":[],\"nightly\":[\"foldhash?/nightly\",\"bumpalo/allocator_api\"],\"raw-entry\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", + "hashlink_0.10.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"serde_impl\":[\"serde\"]}}", "heck_0.5.0": "{\"dependencies\":[],\"features\":{}}", "hermit-abi_0.5.2": "{\"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\"}],\"features\":{\"default\":[],\"rustc-dep-of-std\":[\"core\",\"alloc\"]}}", "hex_0.4.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"faster-hex\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rustc-hex\",\"req\":\"^2.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "hickory-proto_0.25.2": "{\"dependencies\":[{\"name\":\"async-trait\",\"req\":\"^0.1.43\"},{\"default_features\":false,\"features\":[\"prebuilt-nasm\"],\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.12.3\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.50\"},{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2.4.1\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"data-encoding\",\"req\":\"^2.2.0\"},{\"name\":\"enum-as-inner\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-channel\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"features\":[\"stream\"],\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.7\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.9\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"alloc\",\"compiled_data\"],\"name\":\"idna\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"ipnet\",\"req\":\"^2.3.0\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.44\"},{\"default_features\":false,\"features\":[\"critical-section\"],\"name\":\"once_cell\",\"req\":\"^1.20.0\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"log\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.2\"},{\"default_features\":false,\"features\":[\"alloc\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"std\"],\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.23\"},{\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.10\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"time\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1.1\"},{\"features\":[\"io-util\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.21\"},{\"features\":[\"rt\",\"time\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"default_features\":false,\"features\":[\"early-data\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\"},{\"default_features\":false,\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"url\",\"req\":\"^2.5.4\"},{\"name\":\"wasm-bindgen-crate\",\"optional\":true,\"package\":\"wasm-bindgen\",\"req\":\"^0.2.58\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"__dnssec\":[\"dep:bitflags\",\"dep:rustls-pki-types\",\"dep:time\",\"std\"],\"__h3\":[\"dep:h3\",\"dep:h3-quinn\",\"dep:http\",\"std\"],\"__https\":[\"dep:bytes\",\"dep:h2\",\"dep:http\",\"std\"],\"__quic\":[\"dep:bytes\",\"dep:pin-project-lite\",\"dep:quinn\",\"std\"],\"__tls\":[\"dep:bytes\",\"dep:rustls\",\"dep:tokio-rustls\",\"std\",\"tokio\"],\"backtrace\":[\"dep:backtrace\",\"std\"],\"default\":[\"std\",\"tokio\"],\"dnssec-aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/ring-io\",\"__dnssec\"],\"dnssec-ring\":[\"dep:ring\",\"__dnssec\"],\"h3-aws-lc-rs\":[\"quic-aws-lc-rs\",\"__h3\"],\"h3-ring\":[\"quic-ring\",\"__h3\"],\"https-aws-lc-rs\":[\"tls-aws-lc-rs\",\"__https\"],\"https-ring\":[\"tls-ring\",\"__https\"],\"mdns\":[\"socket2/all\",\"std\"],\"no-std-rand\":[\"once_cell/critical-section\",\"dep:critical-section\"],\"quic-aws-lc-rs\":[\"quinn/rustls-aws-lc-rs\",\"tls-aws-lc-rs\",\"__quic\"],\"quic-ring\":[\"quinn/rustls-ring\",\"tls-ring\",\"__quic\"],\"rustls-platform-verifier\":[\"dep:rustls-platform-verifier\",\"std\"],\"serde\":[\"dep:serde\",\"std\",\"url/serde\"],\"std\":[\"data-encoding/std\",\"futures-channel/std\",\"futures-io/std\",\"futures-util/std\",\"ipnet/std\",\"rand/std\",\"rand/thread_rng\",\"ring?/std\",\"thiserror/std\",\"tracing-subscriber/env-filter\",\"tracing-subscriber/fmt\",\"tracing-subscriber/std\",\"tracing/std\",\"url/std\"],\"testing\":[\"std\"],\"text-parsing\":[\"std\"],\"tls-aws-lc-rs\":[\"tokio-rustls/aws-lc-rs\",\"__tls\"],\"tls-ring\":[\"tokio-rustls/ring\",\"__tls\"],\"tokio\":[\"dep:tokio\",\"std\",\"tokio/net\",\"tokio/rt\",\"tokio/time\",\"tokio/rt-multi-thread\"],\"wasm-bindgen\":[\"dep:wasm-bindgen-crate\",\"dep:js-sys\"]}}", + "hickory-resolver_0.25.2": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.50\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"hickory-proto\",\"req\":\"^0.25\"},{\"name\":\"ipconfig\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(windows)\"},{\"features\":[\"sync\"],\"name\":\"moka\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"critical-section\"],\"name\":\"once_cell\",\"req\":\"^1.20.0\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"log\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"system\"],\"name\":\"resolv-conf\",\"optional\":true,\"req\":\"^0.7.0\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.23\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smallvec\",\"req\":\"^1.6\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.21\"},{\"features\":[\"macros\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.14\"},{\"default_features\":false,\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"std\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"__dnssec\":[],\"__h3\":[\"__quic\"],\"__https\":[\"__tls\"],\"__quic\":[\"dep:quinn\",\"__tls\"],\"__tls\":[\"dep:rustls\",\"dep:tokio-rustls\",\"tokio\"],\"backtrace\":[\"dep:backtrace\",\"hickory-proto/backtrace\"],\"default\":[\"system-config\",\"tokio\"],\"dnssec-aws-lc-rs\":[\"hickory-proto/dnssec-aws-lc-rs\",\"__dnssec\"],\"dnssec-ring\":[\"hickory-proto/dnssec-ring\",\"__dnssec\"],\"h3-aws-lc-rs\":[\"hickory-proto/h3-aws-lc-rs\",\"__h3\"],\"h3-ring\":[\"hickory-proto/h3-ring\",\"__h3\"],\"https-aws-lc-rs\":[\"hickory-proto/https-aws-lc-rs\",\"__https\"],\"https-ring\":[\"hickory-proto/https-ring\",\"__https\"],\"quic-aws-lc-rs\":[\"hickory-proto/quic-aws-lc-rs\",\"__quic\",\"quinn/rustls-aws-lc-rs\"],\"quic-ring\":[\"hickory-proto/quic-ring\",\"__quic\",\"quinn/rustls-ring\"],\"rustls-platform-verifier\":[\"hickory-proto/rustls-platform-verifier\"],\"serde\":[\"dep:serde\",\"hickory-proto/serde\"],\"system-config\":[\"dep:ipconfig\",\"dep:resolv-conf\"],\"tls-aws-lc-rs\":[\"hickory-proto/tls-aws-lc-rs\",\"__tls\"],\"tls-ring\":[\"hickory-proto/tls-ring\",\"__tls\"],\"tokio\":[\"dep:tokio\",\"tokio/rt\",\"hickory-proto/tokio\"],\"webpki-roots\":[\"dep:webpki-roots\",\"hickory-proto/webpki-roots\"]}}", "hkdf_0.12.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"hmac\",\"req\":\"^0.12.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"std\":[\"hmac/std\"]}}", "hmac_0.12.1": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"md-5\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha-1\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"reset\":[],\"std\":[\"digest/std\"]}}", "home_0.5.11": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "hostname_0.4.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(unix, target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.61\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"windows-link\",\"req\":\"^0.1.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"default\":[],\"set\":[]}}", "http-body-util_0.1.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\",\"sync\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{\"channel\":[\"dep:tokio\"],\"default\":[],\"full\":[\"channel\"]}}", "http-body_1.0.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"}],\"features\":{}}", + "http-range-header_0.4.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.8.3\"}],\"features\":{}}", "http_0.2.12": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"<=1.8\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}", "http_1.3.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httparse_1.10.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -819,6 +855,7 @@ "insta_1.46.0": "{\"dependencies\":[{\"features\":[\"derive\",\"env\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.1\"},{\"default_features\":false,\"name\":\"console\",\"optional\":true,\"req\":\"^0.15.4\"},{\"name\":\"csv\",\"optional\":true,\"req\":\"^1.1.6\"},{\"name\":\"globset\",\"optional\":true,\"req\":\">=0.4.6, <0.4.17\"},{\"name\":\"once_cell\",\"req\":\"^1.20.2\"},{\"name\":\"pest\",\"optional\":true,\"req\":\"^2.1.3\"},{\"name\":\"pest_derive\",\"optional\":true,\"req\":\"^2.1.0\"},{\"default_features\":false,\"features\":[\"std\",\"unicode\"],\"name\":\"regex\",\"optional\":true,\"req\":\"^1.6.0\"},{\"name\":\"ron\",\"optional\":true,\"req\":\"^0.12.0\"},{\"kind\":\"dev\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.117\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.117\"},{\"features\":[\"inline\"],\"name\":\"similar\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.4.2\"},{\"name\":\"tempfile\",\"req\":\"^3\"},{\"features\":[\"serde\",\"parse\",\"display\"],\"name\":\"toml_edit\",\"optional\":true,\"req\":\"^0.23.0\"},{\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"walkdir\",\"optional\":true,\"req\":\"^2.3.1\"}],\"features\":{\"_cargo_insta_internal\":[\"clap\"],\"colors\":[\"console\"],\"csv\":[\"dep:csv\",\"serde\"],\"default\":[\"colors\"],\"filters\":[\"regex\"],\"glob\":[\"walkdir\",\"globset\"],\"json\":[\"serde\"],\"redactions\":[\"pest\",\"pest_derive\",\"serde\"],\"ron\":[\"dep:ron\",\"serde\"],\"toml\":[\"dep:toml_edit\",\"dep:toml_writer\",\"serde\"],\"yaml\":[\"serde\"]}}", "instability_0.3.9": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.20.10\"},{\"name\":\"indoc\",\"req\":\"^2.0.5\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.92\"},{\"name\":\"quote\",\"req\":\"^1.0.37\"},{\"features\":[\"derive\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.90\"}],\"features\":{}}", "inventory_0.3.20": "{\"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\":{}}", + "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\":[]}}", "iri-string_0.7.8": "{\"dependencies\":[{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.4.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.104\"}],\"features\":{\"alloc\":[\"serde?/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"memchr?/std\",\"serde?/std\"]}}", "is-terminal_0.4.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"atty\",\"req\":\"^0.2.14\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.110\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"termios\"],\"kind\":\"dev\",\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"stdio\"],\"kind\":\"dev\",\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(not(any(windows, target_os = \\\"hermit\\\", target_os = \\\"unknown\\\")))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_Console\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.60\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -844,7 +881,10 @@ "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\"]}}", "libc_0.2.177": "{\"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.6": "{\"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\":{}}", + "libm_0.2.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"no-panic\",\"req\":\"^0.1.35\"}],\"features\":{\"arch\":[],\"default\":[\"arch\"],\"force-soft-floats\":[],\"unstable\":[\"unstable-intrinsics\",\"unstable-float\"],\"unstable-float\":[],\"unstable-intrinsics\":[],\"unstable-public-internals\":[]}}", "libredox_0.1.6": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"call\":[],\"default\":[\"call\",\"std\",\"redox_syscall\"],\"mkns\":[\"ioslice\"],\"std\":[]}}", + "libsqlite3-sys_0.30.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.69\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.1.6\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.72\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"default\":[\"min_sqlite_version_3_14_0\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_14_0\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}", "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.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\":[]}}", "linux-raw-sys_0.9.4": "{\"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\":[],\"image\":[],\"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\":[]}}", @@ -854,6 +894,7 @@ "log_0.4.29": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.63\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sval\",\"optional\":true,\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval\",\"req\":\"^2.16\"},{\"kind\":\"dev\",\"name\":\"sval_derive\",\"req\":\"^2.16\"},{\"default_features\":false,\"name\":\"sval_ref\",\"optional\":true,\"req\":\"^2.16\"},{\"default_features\":false,\"features\":[\"inline-i128\"],\"name\":\"value-bag\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"value-bag\",\"req\":\"^1.12\"}],\"features\":{\"kv\":[],\"kv_serde\":[\"kv_std\",\"value-bag/serde\",\"serde\"],\"kv_std\":[\"std\",\"kv\",\"value-bag/error\"],\"kv_sval\":[\"kv\",\"value-bag/sval\",\"sval\",\"sval_ref\"],\"kv_unstable\":[\"kv\",\"value-bag\"],\"kv_unstable_serde\":[\"kv_serde\",\"kv_unstable_std\"],\"kv_unstable_std\":[\"kv_std\",\"kv_unstable\"],\"kv_unstable_sval\":[\"kv_sval\",\"kv_unstable\"],\"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\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", "logos-derive_0.12.1": "{\"dependencies\":[{\"name\":\"beef\",\"req\":\"^0.5.0\"},{\"name\":\"fnv\",\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.9\"},{\"name\":\"quote\",\"req\":\"^1.0.3\"},{\"name\":\"regex-syntax\",\"req\":\"^0.6\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^1.0.17\"}],\"features\":{}}", "logos_0.12.1": "{\"dependencies\":[{\"name\":\"logos-derive\",\"optional\":true,\"req\":\"^0.12.1\"}],\"features\":{\"default\":[\"export_derive\",\"std\"],\"export_derive\":[\"logos-derive\"],\"std\":[]}}", + "loom_0.7.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.0\"},{\"name\":\"generator\",\"req\":\"^0.8.1\"},{\"name\":\"pin-utils\",\"optional\":true,\"req\":\"^0.1.0\"},{\"name\":\"scoped-tls\",\"req\":\"^1.0.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.92\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.33\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.27\"},{\"features\":[\"env-filter\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.8\"}],\"features\":{\"checkpoint\":[\"serde\",\"serde_json\"],\"default\":[],\"futures\":[\"pin-utils\"]}}", "lru-slab_0.1.2": "{\"dependencies\":[],\"features\":{}}", "lru_0.12.5": "{\"dependencies\":[{\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"scoped_threadpool\",\"req\":\"0.1.*\"},{\"kind\":\"dev\",\"name\":\"stats_alloc\",\"req\":\"0.1.*\"}],\"features\":{\"default\":[\"hashbrown\"],\"nightly\":[\"hashbrown\",\"hashbrown/nightly\"]}}", "lru_0.16.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.16.0\"},{\"kind\":\"dev\",\"name\":\"scoped_threadpool\",\"req\":\"0.1.*\"},{\"kind\":\"dev\",\"name\":\"stats_alloc\",\"req\":\"0.1.*\"}],\"features\":{\"default\":[\"hashbrown\"],\"nightly\":[\"hashbrown\",\"hashbrown/nightly\"]}}", @@ -861,6 +902,9 @@ "maplit_1.0.2": "{\"dependencies\":[],\"features\":{}}", "matchers_0.2.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"syntax\",\"dfa-build\",\"dfa-search\"],\"name\":\"regex-automata\",\"req\":\"^0.4\"}],\"features\":{\"unicode\":[\"regex-automata/unicode\"]}}", "matchit_0.8.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", + "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.7.5": "{\"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\":[]}}", @@ -869,6 +913,7 @@ "minimal-lexical_0.2.1": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"compact\":[],\"default\":[\"std\"],\"lint\":[],\"nightly\":[],\"std\":[]}}", "miniz_oxide_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"adler2\",\"req\":\"^2.0\"},{\"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\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"simd-adler32\",\"optional\":true,\"req\":\"^0.3.3\"}],\"features\":{\"block-boundary\":[],\"default\":[\"with-alloc\"],\"rustc-dep-of-std\":[\"core\",\"alloc\",\"adler2/rustc-dep-of-std\"],\"simd\":[\"simd-adler32\"],\"std\":[],\"with-alloc\":[]}}", "mio_1.0.4": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"name\":\"libc\",\"req\":\"^0.2.159\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.159\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.159\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"wasi\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"wasi\\\")\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Wdk_System_IO\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"log\"],\"net\":[],\"os-ext\":[\"os-poll\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_Security\"],\"os-poll\":[]}}", + "moka_0.12.12": "{\"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.5": "{\"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.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\"]}}", @@ -879,11 +924,12 @@ "nix_0.29.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert-impl\",\"req\":\"^0.1\"},{\"name\":\"bitflags\",\"req\":\"^2.3.1\"},{\"kind\":\"dev\",\"name\":\"caps\",\"req\":\"^0.5.3\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"features\":[\"extra_traits\"],\"name\":\"libc\",\"req\":\"^0.2.155\"},{\"name\":\"memoffset\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-utils\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"sysctl\",\"req\":\"^0.4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.7.1\"}],\"features\":{\"acct\":[],\"aio\":[\"pin-utils\"],\"default\":[],\"dir\":[\"fs\"],\"env\":[],\"event\":[],\"fanotify\":[],\"feature\":[],\"fs\":[],\"hostname\":[],\"inotify\":[],\"ioctl\":[],\"kmod\":[],\"mman\":[],\"mount\":[\"uio\"],\"mqueue\":[\"fs\"],\"net\":[\"socket\"],\"personality\":[],\"poll\":[],\"process\":[],\"pthread\":[],\"ptrace\":[\"process\"],\"quota\":[],\"reboot\":[],\"resource\":[],\"sched\":[\"process\"],\"signal\":[\"process\"],\"socket\":[\"memoffset\"],\"term\":[],\"time\":[],\"ucontext\":[\"signal\"],\"uio\":[],\"user\":[\"feature\"],\"zerocopy\":[\"fs\",\"uio\"]}}", "nix_0.30.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert-impl\",\"req\":\"^0.1\"},{\"name\":\"bitflags\",\"req\":\"^2.3.3\"},{\"kind\":\"dev\",\"name\":\"caps\",\"req\":\"^0.5.3\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2.1\"},{\"features\":[\"extra_traits\"],\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"name\":\"memoffset\",\"optional\":true,\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-utils\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"sysctl\",\"req\":\"^0.4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.7.1\"}],\"features\":{\"acct\":[],\"aio\":[\"pin-utils\"],\"default\":[],\"dir\":[\"fs\"],\"env\":[],\"event\":[\"poll\"],\"fanotify\":[],\"feature\":[],\"fs\":[],\"hostname\":[],\"inotify\":[],\"ioctl\":[],\"kmod\":[],\"mman\":[],\"mount\":[\"uio\"],\"mqueue\":[\"fs\"],\"net\":[\"socket\"],\"personality\":[],\"poll\":[],\"process\":[],\"pthread\":[],\"ptrace\":[\"process\"],\"quota\":[],\"reboot\":[],\"resource\":[],\"sched\":[\"process\"],\"signal\":[\"process\"],\"socket\":[\"memoffset\"],\"syslog\":[],\"term\":[],\"time\":[],\"ucontext\":[\"signal\"],\"uio\":[],\"user\":[\"feature\"],\"zerocopy\":[\"fs\",\"uio\"]}}", "nom_7.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"minimal-lexical\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"docsrs\":[],\"std\":[\"alloc\",\"memchr/std\",\"minimal-lexical/std\"]}}", + "nom_8.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"=1.0.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"docsrs\":[],\"std\":[\"alloc\",\"memchr/std\"]}}", "normalize-line-endings_0.3.0": "{\"dependencies\":[],\"features\":{}}", "notify-types_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.34.0\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.24.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.89\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.39\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1.0\"}],\"features\":{\"serialization-compat-6\":[]}}", "notify_8.2.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.7.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"name\":\"crossbeam-channel\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"flume\",\"optional\":true,\"req\":\"^0.11.1\"},{\"name\":\"fsevent-sys\",\"optional\":true,\"req\":\"^4.0.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"default_features\":false,\"name\":\"inotify\",\"req\":\"^0.11.0\",\"target\":\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"android\\\"))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.34.0\"},{\"name\":\"kqueue\",\"req\":\"^1.1.1\",\"target\":\"cfg(any(target_os=\\\"freebsd\\\", target_os=\\\"openbsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"dragonflybsd\\\", target_os = \\\"ios\\\"))\"},{\"name\":\"kqueue\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.4\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"req\":\"^1.0\",\"target\":\"cfg(any(target_os=\\\"freebsd\\\", target_os=\\\"openbsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"dragonflybsd\\\", target_os = \\\"ios\\\"))\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"req\":\"^1.0\",\"target\":\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"android\\\"))\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\"},{\"name\":\"notify-types\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.39\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.0\"},{\"kind\":\"dev\",\"name\":\"trash\",\"req\":\"^5.2.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"walkdir\",\"req\":\"^2.4.0\"},{\"features\":[\"Win32_System_Threading\",\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_Security\",\"Win32_System_WindowsProgramming\",\"Win32_System_IO\"],\"name\":\"windows-sys\",\"req\":\"^0.60.1\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"macos_fsevent\"],\"macos_fsevent\":[\"fsevent-sys\"],\"macos_kqueue\":[\"kqueue\",\"mio\"],\"serde\":[\"notify-types/serde\"],\"serialization-compat-6\":[\"notify-types/serialization-compat-6\"]}}", "nu-ansi-term_0.50.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.152\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.94\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Storage_FileSystem\",\"Win32_Security\"],\"name\":\"windows\",\"package\":\"windows-sys\",\"req\":\"^0.52.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"derive_serde_style\":[\"serde\"],\"gnu_legacy\":[]}}", - "nucleo-matcher_0.3.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cov-mark\",\"req\":\"^1.1.0\"},{\"name\":\"memchr\",\"req\":\"^2.5.0\"},{\"name\":\"unicode-segmentation\",\"optional\":true,\"req\":\"^1.10\"}],\"features\":{\"default\":[\"unicode-normalization\",\"unicode-casefold\",\"unicode-segmentation\"],\"unicode-casefold\":[],\"unicode-normalization\":[],\"unicode-segmentation\":[\"dep:unicode-segmentation\"]}}", + "num-bigint-dig_0.8.6": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"spin_no_std\"],\"name\":\"lazy_static\",\"req\":\"^1.2.0\"},{\"name\":\"libm\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-integer\",\"req\":\"^0.1.39\"},{\"default_features\":false,\"name\":\"num-iter\",\"req\":\"^0.1.37\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.4\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand_isaac\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"smallvec\",\"req\":\"^1.10.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"default\":[\"std\",\"u64_digit\"],\"fuzz\":[\"arbitrary\",\"smallvec/arbitrary\"],\"i128\":[],\"nightly\":[],\"prime\":[\"rand/std_rng\"],\"std\":[\"num-integer/std\",\"num-traits/std\",\"smallvec/write\",\"rand/std\",\"serde/std\"],\"u64_digit\":[]}}", "num-bigint_0.4.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-integer\",\"req\":\"^0.1.46\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.18\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\"],\"quickcheck\":[\"dep:quickcheck\"],\"rand\":[\"dep:rand\"],\"serde\":[\"dep:serde\"],\"std\":[\"num-integer/std\",\"num-traits/std\"]}}", "num-complex_0.4.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytecheck\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.18\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"bytecheck\":[\"dep:bytecheck\"],\"bytemuck\":[\"dep:bytemuck\"],\"default\":[\"std\"],\"libm\":[\"num-traits/libm\"],\"rand\":[\"dep:rand\"],\"rkyv\":[\"dep:rkyv\"],\"serde\":[\"dep:serde\"],\"std\":[\"num-traits/std\"]}}", "num-conv_0.1.0": "{\"dependencies\":[],\"features\":{}}", @@ -939,6 +985,8 @@ "pin-project_1.1.10": "{\"dependencies\":[{\"name\":\"pin-project-internal\",\"req\":\"=1.1.10\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "pin-utils_0.1.0": "{\"dependencies\":[],\"features\":{}}", "piper_0.2.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-executor\",\"req\":\"^1.5.1\"},{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.0.0\"},{\"name\":\"atomic-waker\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.2.0\"},{\"default_features\":false,\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.28\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"}],\"features\":{\"default\":[\"std\"],\"portable-atomic\":[\"atomic-waker/portable-atomic\",\"portable_atomic_crate\",\"portable-atomic-util\"],\"std\":[\"fastrand/std\",\"futures-io\"]}}", + "pkcs1_0.7.5": "{\"dependencies\":[{\"features\":[\"db\"],\"kind\":\"dev\",\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"spki\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"der/alloc\",\"zeroize\",\"pkcs8?/alloc\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8?/pem\"],\"std\":[\"der/std\",\"alloc\"],\"zeroize\":[\"der/zeroize\"]}}", + "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.7.4": "{\"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.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\"]}}", @@ -960,6 +1008,8 @@ "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.1": "{\"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.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.14.1\"},{\"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.178": "{\"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.23": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2\"}],\"features\":{}}", @@ -972,6 +1022,26 @@ "quote_1.0.40": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\"]}}", "r-efi_5.3.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"efiapi\":[],\"examples\":[\"native\"],\"native\":[],\"rustc-dep-of-std\":[\"core\"]}}", "radix_trie_0.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"endian-type\",\"req\":\"^0.1.2\"},{\"name\":\"nibble_vec\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{}}", + "radix_trie_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"endian-type\",\"req\":\"^0.2.0\"},{\"name\":\"nibble_vec\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{}}", + "rama-boring-sys_0.5.9": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72.0\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"},{\"kind\":\"build\",\"name\":\"fslock\",\"req\":\"^0.2\"}],\"features\":{}}", + "rama-boring-tokio_0.5.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"rama-boring\",\"req\":\"^0.5.9\"},{\"name\":\"rama-boring-sys\",\"req\":\"^0.5.9\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{}}", + "rama-boring_0.5.9": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.9\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8.0\"},{\"name\":\"foreign-types\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"openssl-macros\",\"req\":\"^0.1.1\"},{\"name\":\"rama-boring-sys\",\"req\":\"^0.5.9\"},{\"kind\":\"dev\",\"name\":\"rusty-hook\",\"req\":\"^0.11\"}],\"features\":{}}", + "rama-core_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"asynk-strim\",\"req\":\"^0.1\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"trace\"],\"name\":\"opentelemetry\",\"optional\":true,\"req\":\"^0.31\"},{\"features\":[\"semconv_experimental\"],\"name\":\"opentelemetry-semantic-conventions\",\"optional\":true,\"req\":\"^0.31\"},{\"default_features\":false,\"features\":[\"trace\",\"rt-tokio\"],\"name\":\"opentelemetry_sdk\",\"optional\":true,\"req\":\"^0.31\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-error\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"macros\",\"fs\",\"io-std\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"name\":\"tokio-graceful\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"codec\",\"io\",\"io-util\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-opentelemetry\",\"optional\":true,\"req\":\"^0.32\"}],\"features\":{\"default\":[],\"opentelemetry\":[\"dep:opentelemetry\",\"dep:opentelemetry-semantic-conventions\",\"dep:opentelemetry_sdk\",\"dep:tracing-opentelemetry\"]}}", + "rama-dns_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"tokio\",\"system-config\"],\"name\":\"hickory-resolver\",\"req\":\"^0.25\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_html_form\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"}],\"features\":{\"default\":[]}}", + "rama-error_0.3.0-alpha.4": "{\"dependencies\":[],\"features\":{}}", + "rama-http-backend_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"h2\",\"req\":\"^0.4\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-headers\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-tcp\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-unix\",\"req\":\"^0.3.0-alpha.4\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"default\":[],\"tls\":[\"rama-net/tls\"]}}", + "rama-http-core_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"atomic-waker\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"httparse\",\"req\":\"^1.10\"},{\"name\":\"httpdate\",\"req\":\"^1.0\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"slab\",\"req\":\"^0.4\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5\"},{\"name\":\"want\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1.0\"}],\"features\":{\"default\":[],\"unstable\":[]}}", + "rama-http-headers_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"serde\",\"oldtime\",\"clock\"],\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"name\":\"httpdate\",\"req\":\"^1.0\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-error\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"sha1\",\"req\":\"^0.10\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2\"}],\"features\":{}}", + "rama-http-types_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"name\":\"fnv\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"name\":\"memchr\",\"req\":\"^2.7\"},{\"name\":\"mime\",\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"mime_guess\",\"req\":\"^2\"},{\"name\":\"nom\",\"req\":\"^8.0.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-error\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"features\":[\"macros\",\"io-std\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"default\":[]}}", + "rama-http_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"features\":[\"tokio\",\"brotli\",\"zlib\",\"gzip\",\"zstd\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2.10\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"default_features\":false,\"features\":[\"serde\",\"oldtime\",\"clock\"],\"name\":\"chrono\",\"req\":\"^0.4\"},{\"features\":[\"brotli\",\"deflate\",\"gzip\",\"zstd\"],\"name\":\"compression-codecs\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"compression-core\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"name\":\"csv\",\"req\":\"^1.4\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-range-header\",\"req\":\"^0.4\"},{\"name\":\"httpdate\",\"req\":\"^1.0\"},{\"name\":\"iri-string\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"name\":\"matchit\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"opentelemetry-http\",\"optional\":true,\"req\":\"^0.31\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"radix_trie\",\"req\":\"^0.3\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-error\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-headers\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"kind\":\"dev\",\"name\":\"rama-tcp\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"rawzip\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_html_form\",\"req\":\"^0.3\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.23\"},{\"features\":[\"macros\",\"fs\",\"io-std\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"req\":\"^1.18\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"compression\":[\"dep:async-compression\",\"dep:compression-codecs\",\"dep:compression-core\",\"dep:rawzip\",\"dep:flate2\"],\"default\":[],\"opentelemetry\":[\"rama-core/opentelemetry\",\"rama-net/opentelemetry\",\"dep:opentelemetry-http\"],\"tls\":[\"rama-net/tls\"]}}", + "rama-macros_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste-test-suite\",\"req\":\"^0\"},{\"name\":\"proc-macro-crate\",\"req\":\"^3.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{}}", + "rama-net_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"features\":[\"async\"],\"name\":\"flume\",\"req\":\"^0.12\"},{\"name\":\"hex\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"ipnet\",\"req\":\"^2.11\"},{\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"name\":\"md5\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"nom\",\"req\":\"^8.0.0\"},{\"kind\":\"dev\",\"name\":\"nom\",\"req\":\"^8.0.0\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"psl\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"radix_trie\",\"req\":\"^0.3\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"optional\":true,\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"req\":\"^0.6\"},{\"features\":[\"macros\",\"fs\",\"io-std\",\"io-util\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"venndb\",\"optional\":true,\"req\":\"^0.6\"}],\"features\":{\"default\":[],\"http\":[\"dep:rama-http-types\",\"dep:sha2\",\"dep:hex\",\"dep:md5\"],\"opentelemetry\":[\"rama-core/opentelemetry\"],\"tls\":[\"dep:hex\",\"dep:md5\",\"dep:sha2\"]}}", + "rama-socks5_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.5\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-dns\",\"optional\":true,\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"http\"],\"name\":\"rama-tcp\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-udp\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"default\":[],\"dns\":[\"dep:rama-dns\",\"dep:rand\"]}}", + "rama-tcp_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-dns\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"optional\":true,\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"}],\"features\":{\"default\":[],\"http\":[\"dep:rama-http-types\",\"rama-net/http\"]}}", + "rama-tls-boring_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.1\"},{\"features\":[\"async\"],\"name\":\"flume\",\"req\":\"^0.12\"},{\"name\":\"itertools\",\"req\":\"^0.14\"},{\"features\":[\"sync\"],\"name\":\"moka\",\"req\":\"^0.12\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-boring\",\"req\":\"^0.5.7\"},{\"name\":\"rama-boring-tokio\",\"req\":\"^0.5.7\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-http-types\",\"optional\":true,\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"tls\",\"http\"],\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"tls\"],\"name\":\"rama-ua\",\"optional\":true,\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-utils\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"macros\",\"io-std\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"compression\":[\"dep:flate2\",\"dep:brotli\",\"dep:zstd\"],\"default\":[],\"http\":[\"dep:rama-http-types\"],\"ua\":[\"dep:rama-ua\"]}}", + "rama-udp_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"net\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"}],\"features\":{\"default\":[]}}", + "rama-unix_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"}],\"features\":{\"default\":[]}}", + "rama-utils_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"regex\",\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"},{\"features\":[\"write\",\"serde\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"name\":\"smol_str\",\"req\":\"^0.3\"},{\"features\":[\"time\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wildcard\",\"req\":\"^0.3\"}],\"features\":{}}", "rand_0.8.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.22\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"features\":[\"into_bits\"],\"name\":\"packed_simd\",\"optional\":true,\"package\":\"packed_simd_2\",\"req\":\"^0.3.7\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"}],\"features\":{\"alloc\":[\"rand_core/alloc\"],\"default\":[\"std\",\"std_rng\"],\"getrandom\":[\"rand_core/getrandom\"],\"min_const_gen\":[],\"nightly\":[],\"serde1\":[\"serde\",\"rand_core/serde1\"],\"simd_support\":[\"packed_simd\"],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha/std\",\"alloc\",\"getrandom\",\"libc\"],\"std_rng\":[\"rand_chacha\"]}}", "rand_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"std_rng\",\"os_rng\",\"small_rng\",\"thread_rng\"],\"log\":[\"dep:log\"],\"nightly\":[],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\",\"rand_core/serde\"],\"simd_support\":[],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha?/std\",\"alloc\"],\"std_rng\":[\"dep:rand_chacha\"],\"thread_rng\":[\"std\",\"std_rng\",\"os_rng\"],\"unbiased\":[]}}", "rand_chacha_0.3.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.8\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde1\":[\"serde\"],\"simd\":[],\"std\":[\"ppv-lite86/std\"]}}", @@ -980,6 +1050,8 @@ "rand_core_0.9.3": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"os_rng\":[\"dep:getrandom\"],\"serde\":[\"dep:serde\"],\"std\":[\"getrandom?/std\"]}}", "rand_xorshift_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.118\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", "ratatui-macros_0.6.0": "{\"dependencies\":[{\"features\":[\"user-hooks\"],\"kind\":\"dev\",\"name\":\"cargo-husky\",\"req\":\"^1.5.0\"},{\"name\":\"ratatui\",\"req\":\"^0.29.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.101\"}],\"features\":{}}", + "rayon-core_1.13.0": "{\"dependencies\":[{\"name\":\"crossbeam-deque\",\"req\":\"^0.8.1\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"scoped-tls\",\"req\":\"^1.0\"},{\"name\":\"wasm_sync\",\"optional\":true,\"req\":\"^0.1.0\"}],\"features\":{\"web_spin_lock\":[\"dep:wasm_sync\"]}}", + "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\"]}}", "redox_syscall_0.5.15": "{\"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.0": "{\"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\"]}}", @@ -991,9 +1063,11 @@ "regex-syntax_0.8.5": "{\"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.2": "{\"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.24": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"tokio\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"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.21.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\",\"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\":[\"codec\",\"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.5\",\"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\":[\"dep:async-compression\",\"async-compression?/brotli\",\"dep:futures-util\",\"dep:tokio-util\"],\"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\":[\"dep:async-compression\",\"async-compression?/zlib\",\"dep:futures-util\",\"dep:tokio-util\"],\"gzip\":[\"dep:async-compression\",\"async-compression?/gzip\",\"dep:futures-util\",\"dep:tokio-util\"],\"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\":[\"dep:async-compression\",\"async-compression?/zstd\",\"dep:futures-util\",\"dep:tokio-util\"]}}", + "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", "rmcp-macros_0.12.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "rmcp_0.12.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.12.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}", + "rsa_0.9.10": "{\"dependencies\":[{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"base64ct\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"oid\"],\"name\":\"digest\",\"req\":\"^0.10.5\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"default_features\":false,\"features\":[\"i128\",\"prime\",\"zeroize\"],\"name\":\"num-bigint\",\"package\":\"num-bigint-dig\",\"req\":\"^0.8.6\"},{\"default_features\":false,\"name\":\"num-integer\",\"req\":\"^0.1.39\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"req\":\"^0.2.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"pkcs8\"],\"name\":\"pkcs1\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"pkcs8\",\"req\":\"^0.10.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.184\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.89\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10.7\"},{\"default_features\":false,\"features\":[\"alloc\",\"digest\",\"rand_core\"],\"name\":\"signature\",\"req\":\">2.0, <2.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"spki\",\"req\":\"^0.7.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.1.1\"},{\"features\":[\"alloc\"],\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"default\":[\"std\",\"pem\",\"u64_digit\"],\"getrandom\":[\"rand_core/getrandom\"],\"hazmat\":[],\"nightly\":[\"num-bigint/nightly\"],\"pem\":[\"pkcs1/pem\",\"pkcs8/pem\"],\"pkcs5\":[\"pkcs8/encryption\"],\"serde\":[\"dep:serde\",\"num-bigint/serde\"],\"std\":[\"digest/std\",\"pkcs1/std\",\"pkcs8/std\",\"rand_core/std\",\"signature/std\"],\"u64_digit\":[\"num-bigint/u64_digit\"]}}", "rustc-demangle_0.1.25": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"compiler_builtins\":[],\"rustc-dep-of-std\":[\"core\"],\"std\":[]}}", "rustc-hash_2.1.1": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"rand\":[\"dep:rand\",\"std\"],\"std\":[]}}", "rustc_version_0.4.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"semver\",\"req\":\"^1.0\"}],\"features\":{}}", @@ -1017,6 +1091,7 @@ "schemars_1.0.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec07\",\"optional\":true,\"package\":\"arrayvec\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"arrayvec07\",\"package\":\"arrayvec\",\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"bigdecimal04\",\"optional\":true,\"package\":\"bigdecimal\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"bigdecimal04\",\"package\":\"bigdecimal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"bytes1\",\"optional\":true,\"package\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"bytes1\",\"package\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"chrono04\",\"optional\":true,\"package\":\"chrono\",\"req\":\"^0.4.39\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono04\",\"package\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"dyn-clone\",\"req\":\"^1.0.17\"},{\"default_features\":false,\"name\":\"either1\",\"optional\":true,\"package\":\"either\",\"req\":\"^1.3\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"either1\",\"package\":\"either\",\"req\":\"^1.3\"},{\"features\":[\"derive\",\"email\",\"regex\",\"url\"],\"kind\":\"dev\",\"name\":\"garde\",\"req\":\"^0.22\"},{\"default_features\":false,\"name\":\"indexmap2\",\"optional\":true,\"package\":\"indexmap\",\"req\":\"^2.2.3\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"indexmap2\",\"package\":\"indexmap\",\"req\":\"^2.0\"},{\"default_features\":false,\"name\":\"jiff02\",\"optional\":true,\"package\":\"jiff\",\"req\":\"^0.2\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"jiff02\",\"package\":\"jiff\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"jsonschema\",\"req\":\"^0.30\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.2.1\"},{\"name\":\"ref-cast\",\"req\":\"^1.0.22\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.6\"},{\"default_features\":false,\"name\":\"rust_decimal1\",\"optional\":true,\"package\":\"rust_decimal\",\"req\":\"^1.13\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"rust_decimal1\",\"package\":\"rust_decimal\",\"req\":\"^1\"},{\"name\":\"schemars_derive\",\"optional\":true,\"req\":\"=1.0.4\"},{\"default_features\":false,\"name\":\"semver1\",\"optional\":true,\"package\":\"semver\",\"req\":\"^1.0.9\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"semver1\",\"package\":\"semver\",\"req\":\"^1.0.9\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"req\":\"^1.0.194\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_json\",\"req\":\"^1.0.127\"},{\"kind\":\"dev\",\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"default_features\":false,\"name\":\"smallvec1\",\"optional\":true,\"package\":\"smallvec\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"smallvec1\",\"package\":\"smallvec\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"smol_str02\",\"optional\":true,\"package\":\"smol_str\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"smol_str02\",\"package\":\"smol_str\",\"req\":\"^0.2.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.17\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"url2\",\"optional\":true,\"package\":\"url\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"serde\",\"std\"],\"kind\":\"dev\",\"name\":\"url2\",\"package\":\"url\",\"req\":\"^2.0\"},{\"default_features\":false,\"name\":\"uuid1\",\"optional\":true,\"package\":\"uuid\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"uuid1\",\"package\":\"uuid\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"validator\",\"req\":\"^0.20\"}],\"features\":{\"_ui_test\":[],\"default\":[\"derive\",\"std\"],\"derive\":[\"schemars_derive\"],\"preserve_order\":[\"serde_json/preserve_order\"],\"raw_value\":[\"serde_json/raw_value\"],\"std\":[]}}", "schemars_derive_0.8.22": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.2.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"serde_derive_internals\",\"req\":\"^0.29\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "schemars_derive_1.0.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.2.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"serde_derive_internals\",\"req\":\"^0.29.1\"},{\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"extra-traits\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", + "scoped-tls_1.0.1": "{\"dependencies\":[],\"features\":{}}", "scopeguard_1.2.0": "{\"dependencies\":[],\"features\":{\"default\":[\"use_std\"],\"use_std\":[]}}", "sdd_3.0.10": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{}}", "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\"]}}", @@ -1038,6 +1113,7 @@ "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\":[]}}", "serde_derive_internals_0.29.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"derive\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{}}", + "serde_html_form_0.3.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches2\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.11\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"form_urlencoded\",\"req\":\"^1.0.1\"},{\"default_features\":false,\"name\":\"indexmap\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.45.0\"},{\"name\":\"itoa\",\"req\":\"^1.0.1\"},{\"name\":\"ryu\",\"optional\":true,\"req\":\"^1.0.9\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"}],\"features\":{\"default\":[\"ryu\",\"std\"],\"std\":[]}}", "serde_json_1.0.145": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.11\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.2.3\"},{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2.0.2\"},{\"name\":\"itoa\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"ref-cast\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"name\":\"ryu\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.194\"},{\"kind\":\"dev\",\"name\":\"serde_bytes\",\"req\":\"^0.11.10\"},{\"default_features\":false,\"name\":\"serde_core\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_stacker\",\"req\":\"^0.1.8\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"alloc\":[\"serde_core/alloc\"],\"arbitrary_precision\":[],\"default\":[\"std\"],\"float_roundtrip\":[],\"preserve_order\":[\"indexmap\",\"std\"],\"raw_value\":[],\"std\":[\"memchr/std\",\"serde_core/std\"],\"unbounded_depth\":[]}}", "serde_path_to_error_0.1.20": "{\"dependencies\":[{\"name\":\"itoa\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.100\"}],\"features\":{}}", "serde_repr_0.1.20": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.100\"},{\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.81\"}],\"features\":{}}", @@ -1059,6 +1135,7 @@ "signal-hook-mio_0.2.4": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"~0.2\"},{\"name\":\"mio-0_6\",\"optional\":true,\"package\":\"mio\",\"req\":\"~0.6\"},{\"features\":[\"os-util\",\"uds\"],\"name\":\"mio-0_7\",\"optional\":true,\"package\":\"mio\",\"req\":\"~0.7\"},{\"features\":[\"os-util\",\"os-poll\",\"uds\"],\"kind\":\"dev\",\"name\":\"mio-0_7\",\"package\":\"mio\",\"req\":\"~0.7\"},{\"features\":[\"net\",\"os-ext\"],\"name\":\"mio-0_8\",\"optional\":true,\"package\":\"mio\",\"req\":\"~0.8\"},{\"features\":[\"net\",\"os-ext\"],\"name\":\"mio-1_0\",\"optional\":true,\"package\":\"mio\",\"req\":\"~1.0\"},{\"name\":\"mio-uds\",\"optional\":true,\"req\":\"~0.6\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"~0.5\"},{\"name\":\"signal-hook\",\"req\":\"~0.3\"}],\"features\":{\"support-v0_6\":[\"mio-0_6\",\"mio-uds\"],\"support-v0_7\":[\"mio-0_7\"],\"support-v0_8\":[\"mio-0_8\"],\"support-v1_0\":[\"mio-1_0\"]}}", "signal-hook-registry_1.4.5": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"~0.3\"}],\"features\":{}}", "signal-hook_0.3.18": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^0.7\"},{\"name\":\"signal-hook-registry\",\"req\":\"^1.4\"}],\"features\":{\"channel\":[],\"default\":[\"channel\",\"iterator\"],\"extended-siginfo\":[\"channel\",\"iterator\",\"extended-siginfo-raw\"],\"extended-siginfo-raw\":[\"cc\"],\"iterator\":[\"channel\"]}}", + "signature_2.2.0": "{\"dependencies\":[{\"name\":\"derive\",\"optional\":true,\"package\":\"signature_derive\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10.6\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\",\"rand_core?/std\"]}}", "simd-adler32_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adler\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"adler32\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"const-generics\":[],\"default\":[\"std\",\"const-generics\"],\"nightly\":[],\"std\":[]}}", "simdutf8_0.1.5": "{\"dependencies\":[],\"features\":{\"aarch64_neon\":[],\"aarch64_neon_prefetch\":[],\"default\":[\"std\"],\"hints\":[],\"public_imp\":[],\"std\":[]}}", "similar_2.7.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"console\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.10.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.68\"},{\"name\":\"unicode-segmentation\",\"optional\":true,\"req\":\"^1.7.1\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1\"}],\"features\":{\"bytes\":[\"bstr\",\"text\"],\"default\":[\"text\"],\"inline\":[\"text\"],\"text\":[],\"unicode\":[\"text\",\"unicode-segmentation\",\"bstr?/unicode\",\"bstr?/std\"],\"wasm32_web_time\":[\"web-time\"]}}", @@ -1066,8 +1143,18 @@ "slab_0.4.11": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "smallvec_1.15.1": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bincode\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"bincode1\",\"package\":\"bincode\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1.0\"},{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"unty\",\"optional\":true,\"req\":\"^0.0.4\"}],\"features\":{\"const_generics\":[],\"const_new\":[\"const_generics\"],\"debugger_visualizer\":[],\"drain_filter\":[],\"drain_keep_rest\":[\"drain_filter\"],\"impl_bincode\":[\"bincode\",\"unty\"],\"may_dangle\":[],\"specialization\":[],\"union\":[],\"write\":[]}}", "smawk_0.3.2": "{\"dependencies\":[{\"name\":\"ndarray\",\"optional\":true,\"req\":\"^0.15.4\"},{\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2.14\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9.4\"}],\"features\":{}}", + "smol_str_0.3.5": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde_core\"],\"std\":[\"serde_core?/std\",\"borsh?/std\"]}}", "socket2_0.5.10": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.171\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", "socket2_0.6.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.172\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", + "spin_0.9.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"name\":\"lock_api_crate\",\"optional\":true,\"package\":\"lock_api\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"barrier\":[\"mutex\"],\"default\":[\"lock_api\",\"mutex\",\"spin_mutex\",\"rwlock\",\"once\",\"lazy\",\"barrier\"],\"fair_mutex\":[\"mutex\"],\"lazy\":[\"once\"],\"lock_api\":[\"lock_api_crate\"],\"mutex\":[],\"once\":[],\"portable_atomic\":[\"portable-atomic\"],\"rwlock\":[],\"spin_mutex\":[\"mutex\"],\"std\":[],\"ticket_mutex\":[\"mutex\"],\"use_ticket_mutex\":[\"mutex\",\"ticket_mutex\"]}}", + "spki_0.7.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"base64ct\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"base64ct?/alloc\",\"der/alloc\"],\"arbitrary\":[\"std\",\"dep:arbitrary\",\"der/arbitrary\"],\"base64\":[\"dep:base64ct\"],\"fingerprint\":[\"sha2\"],\"pem\":[\"alloc\",\"der/pem\"],\"std\":[\"der/std\",\"alloc\"]}}", + "sqlx-core_0.8.6": "{\"dependencies\":[{\"name\":\"async-io\",\"optional\":true,\"req\":\"^1.9.0\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3\"},{\"name\":\"crossbeam-queue\",\"req\":\"^0.3.2\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"event-listener\",\"req\":\"^5.2.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"name\":\"futures-io\",\"req\":\"^0.3.24\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"name\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"name\":\"hashlink\",\"req\":\"^0.10.0\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.20.0\"},{\"default_features\":false,\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.10\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.15\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.132\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.73\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"postgres\",\"sqlite\",\"mysql\",\"migrate\",\"macros\",\"time\",\"uuid\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"fs\"],\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.8\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"_rt-async-std\":[\"async-std\",\"async-io\"],\"_rt-tokio\":[\"tokio\",\"tokio-stream\"],\"_tls-native-tls\":[\"native-tls\"],\"_tls-none\":[],\"_tls-rustls\":[\"rustls\"],\"_tls-rustls-aws-lc-rs\":[\"_tls-rustls\",\"rustls/aws-lc-rs\",\"webpki-roots\"],\"_tls-rustls-ring-native-roots\":[\"_tls-rustls\",\"rustls/ring\",\"rustls-native-certs\"],\"_tls-rustls-ring-webpki\":[\"_tls-rustls\",\"rustls/ring\",\"webpki-roots\"],\"any\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"],\"migrate\":[\"sha2\",\"crc\"],\"offline\":[\"serde\",\"either/serde\"]}}", + "sqlx-macros-core_0.8.6": "{\"dependencies\":[{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.79\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.26\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.132\"},{\"name\":\"serde_json\",\"req\":\"^1.0.73\"},{\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"features\":[\"offline\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.8.6\"},{\"default_features\":false,\"features\":[\"full\",\"derive\",\"parsing\",\"printing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2.0.52\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-std\":[\"async-std\",\"sqlx-core/_rt-async-std\"],\"_rt-tokio\":[\"tokio\",\"sqlx-core/_rt-tokio\"],\"_sqlite\":[],\"_tls-native-tls\":[\"sqlx-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[],\"derive\":[],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[],\"migrate\":[\"sqlx-core/migrate\"],\"mysql\":[\"sqlx-mysql\"],\"postgres\":[\"sqlx-postgres\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\"],\"time\":[\"sqlx-core/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", + "sqlx-macros_0.8.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.36\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.26\"},{\"features\":[\"any\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-macros-core\",\"req\":\"=0.8.6\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{\"_rt-async-std\":[\"sqlx-macros-core/_rt-async-std\"],\"_rt-tokio\":[\"sqlx-macros-core/_rt-tokio\"],\"_tls-native-tls\":[\"sqlx-macros-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-macros-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-macros-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-macros-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-macros-core/bigdecimal\"],\"bit-vec\":[\"sqlx-macros-core/bit-vec\"],\"chrono\":[\"sqlx-macros-core/chrono\"],\"default\":[],\"derive\":[\"sqlx-macros-core/derive\"],\"ipnet\":[\"sqlx-macros-core/ipnet\"],\"ipnetwork\":[\"sqlx-macros-core/ipnetwork\"],\"json\":[\"sqlx-macros-core/json\"],\"mac_address\":[\"sqlx-macros-core/mac_address\"],\"macros\":[\"sqlx-macros-core/macros\"],\"migrate\":[\"sqlx-macros-core/migrate\"],\"mysql\":[\"sqlx-macros-core/mysql\"],\"postgres\":[\"sqlx-macros-core/postgres\"],\"rust_decimal\":[\"sqlx-macros-core/rust_decimal\"],\"sqlite\":[\"sqlx-macros-core/sqlite\"],\"sqlite-unbundled\":[\"sqlx-macros-core/sqlite-unbundled\"],\"time\":[\"sqlx-macros-core/time\"],\"uuid\":[\"sqlx-macros-core/uuid\"]}}", + "sqlx-mysql_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"bitflags\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"digest\",\"req\":\"^0.10.0\"},{\"name\":\"dotenvy\",\"req\":\"^0.15.5\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-io\",\"req\":\"^0.3.24\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14.4\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.12.0\"},{\"default_features\":false,\"name\":\"hmac\",\"req\":\"^0.12.0\"},{\"name\":\"itoa\",\"req\":\"^1.0.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"name\":\"rsa\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.144\"},{\"default_features\":false,\"name\":\"sha1\",\"req\":\"^0.10.1\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"mysql\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^1.2.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"sqlx-core/bigdecimal\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde/derive\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx-postgres_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.3\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"etcetera\",\"req\":\"^0.8.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.12.0\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12.0\"},{\"name\":\"home\",\"req\":\"^0.5.5\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.20.0\"},{\"name\":\"itoa\",\"req\":\"^1.0.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.144\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"req\":\"^1.0.85\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"features\":[\"serde\"],\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"postgres\",\"derive\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"features\":[\"json\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^1.2.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"dep:num-bigint\",\"sqlx-core/bigdecimal\"],\"bit-vec\":[\"dep:bit-vec\",\"sqlx-core/bit-vec\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"ipnet\":[\"dep:ipnet\",\"sqlx-core/ipnet\"],\"ipnetwork\":[\"dep:ipnetwork\",\"sqlx-core/ipnetwork\"],\"json\":[\"sqlx-core/json\"],\"mac_address\":[\"dep:mac_address\",\"sqlx-core/mac_address\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx-sqlite_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"default_features\":false,\"features\":[\"async\"],\"name\":\"flume\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-executor\",\"req\":\"^0.3.19\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"features\":[\"pkg-config\",\"vcpkg\",\"unlock_notify\"],\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.145\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"macros\",\"runtime-tokio\",\"tls-none\",\"sqlite\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bundled\":[\"libsqlite3-sys/bundled\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde\"],\"preupdate-hook\":[\"libsqlite3-sys/preupdate_hook\"],\"regexp\":[\"dep:regex\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"unbundled\":[\"libsqlite3-sys/buildtime_bindgen\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx_0.8.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.52\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.12\"},{\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"dotenvy\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.19\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\"},{\"features\":[\"bundled-sqlcipher\"],\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\",\"target\":\"cfg(sqlite_test_sqlcipher)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand_xoshiro\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.132\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.73\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-macros\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.8.6\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"kind\":\"dev\",\"name\":\"time_\",\"package\":\"time\",\"req\":\"^0.3.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.53\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-std\":[],\"_rt-tokio\":[],\"_sqlite\":[],\"_unstable-all-types\":[\"bigdecimal\",\"rust_decimal\",\"json\",\"time\",\"chrono\",\"ipnet\",\"ipnetwork\",\"mac_address\",\"uuid\",\"bit-vec\",\"bstr\"],\"all-databases\":[\"mysql\",\"sqlite\",\"postgres\",\"any\"],\"any\":[\"sqlx-core/any\",\"sqlx-mysql?/any\",\"sqlx-postgres?/any\",\"sqlx-sqlite?/any\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-macros?/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-macros?/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"bstr\":[\"sqlx-core/bstr\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-macros?/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[\"any\",\"macros\",\"migrate\",\"json\"],\"derive\":[\"sqlx-macros/derive\"],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-macros?/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-macros?/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-macros?/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-macros?/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[\"derive\",\"sqlx-macros/macros\"],\"migrate\":[\"sqlx-core/migrate\",\"sqlx-macros?/migrate\",\"sqlx-mysql?/migrate\",\"sqlx-postgres?/migrate\",\"sqlx-sqlite?/migrate\"],\"mysql\":[\"sqlx-mysql\",\"sqlx-macros?/mysql\"],\"postgres\":[\"sqlx-postgres\",\"sqlx-macros?/postgres\"],\"regexp\":[\"sqlx-sqlite?/regexp\"],\"runtime-async-std\":[\"_rt-async-std\",\"sqlx-core/_rt-async-std\",\"sqlx-macros?/_rt-async-std\"],\"runtime-async-std-native-tls\":[\"runtime-async-std\",\"tls-native-tls\"],\"runtime-async-std-rustls\":[\"runtime-async-std\",\"tls-rustls-ring\"],\"runtime-tokio\":[\"_rt-tokio\",\"sqlx-core/_rt-tokio\",\"sqlx-macros?/_rt-tokio\"],\"runtime-tokio-native-tls\":[\"runtime-tokio\",\"tls-native-tls\"],\"runtime-tokio-rustls\":[\"runtime-tokio\",\"tls-rustls-ring\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-macros?/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\",\"sqlx-macros?/sqlite\"],\"sqlite-preupdate-hook\":[\"sqlx-sqlite/preupdate-hook\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\",\"sqlx-macros?/sqlite-unbundled\"],\"time\":[\"sqlx-core/time\",\"sqlx-macros?/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"tls-native-tls\":[\"sqlx-core/_tls-native-tls\",\"sqlx-macros?/_tls-native-tls\"],\"tls-none\":[],\"tls-rustls\":[\"tls-rustls-ring\"],\"tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\",\"sqlx-macros?/_tls-rustls-aws-lc-rs\"],\"tls-rustls-ring\":[\"tls-rustls-ring-webpki\"],\"tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\",\"sqlx-macros?/_tls-rustls-ring-native-roots\"],\"tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\",\"sqlx-macros?/_tls-rustls-ring-webpki\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-macros?/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", "sse-stream_0.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"features\":[\"tracing\"],\"kind\":\"dev\",\"name\":\"axum\",\"req\":\"^0.8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"client\",\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"default\":[],\"tracing\":[\"dep:tracing\"]}}", "stable_deref_trait_1.2.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "starlark_0.13.0": "{\"dependencies\":[{\"features\":[\"bumpalo\",\"num-bigint\"],\"name\":\"allocative\",\"req\":\"^0.3.4\"},{\"name\":\"anyhow\",\"req\":\"^1.0.65\"},{\"name\":\"bumpalo\",\"req\":\"^3.8\"},{\"name\":\"cmp_any\",\"req\":\"^0.8.1\"},{\"name\":\"debugserver-types\",\"req\":\"^0.5.0\"},{\"name\":\"derivative\",\"req\":\"^2.2\"},{\"features\":[\"full\"],\"name\":\"derive_more\",\"req\":\"^1.0.0\"},{\"name\":\"display_container\",\"req\":\"^0.9.0\"},{\"name\":\"dupe\",\"req\":\"^0.9.0\"},{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"erased-serde\",\"req\":\"^0.3.12\"},{\"features\":[\"raw\"],\"name\":\"hashbrown\",\"req\":\"^0.14.3\"},{\"name\":\"inventory\",\"req\":\"^0.3.8\"},{\"name\":\"itertools\",\"req\":\"^0.13.0\"},{\"name\":\"maplit\",\"req\":\"^1.0.2\"},{\"name\":\"memoffset\",\"req\":\"^0.6.4\"},{\"name\":\"num-bigint\",\"req\":\"^0.4.3\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"name\":\"ref-cast\",\"req\":\"^1.0.18\"},{\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"name\":\"rustyline\",\"req\":\"^14.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"starlark_derive\",\"req\":\"^0.13.0\"},{\"name\":\"starlark_map\",\"req\":\"^0.13.0\"},{\"name\":\"starlark_syntax\",\"req\":\"^0.13.0\"},{\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"req\":\"^0.10.0\"},{\"name\":\"textwrap\",\"req\":\"^0.11\"},{\"name\":\"thiserror\",\"req\":\"^1.0.36\"}],\"features\":{}}", @@ -1077,6 +1164,7 @@ "static_assertions_1.1.0": "{\"dependencies\":[],\"features\":{\"nightly\":[]}}", "streaming-iterator_0.1.9": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "string_cache_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"serde_support\"],\"serde_support\":[\"serde\"]}}", + "stringprep_0.1.5": "{\"dependencies\":[{\"name\":\"unicode-bidi\",\"req\":\"^0.3\"},{\"name\":\"unicode-normalization\",\"req\":\"^0.1\"},{\"name\":\"unicode-properties\",\"req\":\"^0.1.1\"}],\"features\":{}}", "strsim_0.10.0": "{\"dependencies\":[],\"features\":{}}", "strsim_0.11.1": "{\"dependencies\":[],\"features\":{}}", "strum_0.26.3": "{\"dependencies\":[{\"features\":[\"macros\"],\"name\":\"phf\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"strum_macros\",\"optional\":true,\"req\":\"^0.26.3\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.26\"}],\"features\":{\"default\":[\"std\"],\"derive\":[\"strum_macros\"],\"std\":[]}}", @@ -1093,6 +1181,7 @@ "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.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.23.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.0.0\",\"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\":{}}", @@ -1119,11 +1208,12 @@ "tinystr_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,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.110\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.1\"}],\"features\":{\"alloc\":[\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde\"],\"std\":[],\"zerovec\":[\"dep:zerovec\"]}}", "tinyvec_1.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"smallvec\",\"req\":\"^1\"},{\"name\":\"tinyvec_macros\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"alloc\":[\"tinyvec_macros\"],\"debugger_visualizer\":[],\"default\":[],\"experimental_write_impl\":[],\"grab_spare_slice\":[],\"latest_stable_rust\":[\"rustc_1_61\"],\"nightly_slice_partition_dedup\":[],\"real_blackbox\":[\"criterion/real_blackbox\"],\"rustc_1_40\":[],\"rustc_1_55\":[],\"rustc_1_57\":[],\"rustc_1_61\":[\"rustc_1_57\"],\"std\":[\"alloc\"]}}", "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\":[{\"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.2": "{\"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.13\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.22\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.26\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"logging\",\"tls12\",\"aws_lc_rs\"],\"early-data\":[],\"fips\":[\"rustls/fips\"],\"logging\":[\"rustls/logging\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"rustls/tls12\"]}}", "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.4": "{\"dependencies\":[{\"name\":\"async-stream\",\"req\":\"^0.3.3\"},{\"name\":\"bytes\",\"req\":\"^1.0.0\"},{\"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-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\"]}}", @@ -1141,14 +1231,14 @@ "tower_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.22\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.22\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.22\"},{\"default_features\":false,\"name\":\"hdrhistogram\",\"optional\":true,\"req\":\"^7.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"hdrhistogram\",\"req\":\"^7.0\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"sync_wrapper\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6.2\"},{\"features\":[\"macros\",\"sync\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.6.2\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"fmt\",\"ansi\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"__common\":[\"futures-core\",\"pin-project-lite\"],\"balance\":[\"discover\",\"load\",\"ready-cache\",\"make\",\"slab\",\"util\"],\"buffer\":[\"__common\",\"tokio/sync\",\"tokio/rt\",\"tokio-util\",\"tracing\"],\"discover\":[\"__common\"],\"filter\":[\"__common\",\"futures-util\"],\"full\":[\"balance\",\"buffer\",\"discover\",\"filter\",\"hedge\",\"limit\",\"load\",\"load-shed\",\"make\",\"ready-cache\",\"reconnect\",\"retry\",\"spawn-ready\",\"steer\",\"timeout\",\"util\"],\"hedge\":[\"util\",\"filter\",\"futures-util\",\"hdrhistogram\",\"tokio/time\",\"tracing\"],\"limit\":[\"__common\",\"tokio/time\",\"tokio/sync\",\"tokio-util\",\"tracing\"],\"load\":[\"__common\",\"tokio/time\",\"tracing\"],\"load-shed\":[\"__common\"],\"log\":[\"tracing/log\"],\"make\":[\"futures-util\",\"pin-project-lite\",\"tokio/io-std\"],\"ready-cache\":[\"futures-core\",\"futures-util\",\"indexmap\",\"tokio/sync\",\"tracing\",\"pin-project-lite\"],\"reconnect\":[\"make\",\"tokio/io-std\",\"tracing\"],\"retry\":[\"__common\",\"tokio/time\",\"util\"],\"spawn-ready\":[\"__common\",\"futures-util\",\"tokio/sync\",\"tokio/rt\",\"util\",\"tracing\"],\"steer\":[],\"timeout\":[\"pin-project-lite\",\"tokio/time\"],\"util\":[\"__common\",\"futures-util\",\"pin-project-lite\",\"sync_wrapper\"]}}", "tracing-appender_0.2.3": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"name\":\"crossbeam-channel\",\"req\":\"^0.5.6\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"thiserror\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"formatting\",\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"fmt\",\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.18\"}],\"features\":{}}", "tracing-attributes_0.1.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"visit-mut\",\"clone-impls\",\"extra-traits\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.64\"}],\"features\":{\"async-await\":[]}}", - "tracing-core_0.1.35": "{\"dependencies\":[{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.13.0\"},{\"default_features\":false,\"name\":\"valuable\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"default\":[\"std\",\"valuable?/std\"],\"std\":[\"once_cell\"]}}", + "tracing-core_0.1.36": "{\"dependencies\":[{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.13.0\"},{\"default_features\":false,\"name\":\"valuable\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(tracing_unstable)\"}],\"features\":{\"default\":[\"std\",\"valuable?/std\"],\"std\":[\"once_cell\"]}}", "tracing-error_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"registry\",\"fmt\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"}],\"features\":{\"default\":[\"traced-error\"],\"traced-error\":[]}}", "tracing-log_0.2.0": "{\"dependencies\":[{\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.6\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"lru\",\"optional\":true,\"req\":\"^0.7.7\"},{\"name\":\"once_cell\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.28\"}],\"features\":{\"default\":[\"log-tracer\",\"std\"],\"interest-cache\":[\"lru\",\"ahash\"],\"log-tracer\":[],\"std\":[\"log/std\"]}}", "tracing-opentelemetry_0.32.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1.56\"},{\"default_features\":false,\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.17\"},{\"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\"],\"name\":\"opentelemetry_sdk\",\"req\":\"^0.31.0\"},{\"default_features\":false,\"features\":[\"trace\",\"rt-tokio\",\"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\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.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.0\"},{\"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\",\"opentelemetry_sdk/metrics\",\"smallvec\"]}}", "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.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.43": "{\"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.35\"},{\"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\"]}}", + "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.0": "{\"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-highlight_0.25.10": "{\"dependencies\":[{\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"streaming-iterator\",\"req\":\"^0.1.9\"},{\"name\":\"thiserror\",\"req\":\"^2.0.11\"},{\"name\":\"tree-sitter\",\"req\":\"^0.25.10\"}],\"features\":{}}", "tree-sitter-language_0.1.5": "{\"dependencies\":[],\"features\":{}}", @@ -1162,8 +1252,11 @@ "uname_0.1.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"}],\"features\":{}}", "unarray_0.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"test-strategy\",\"req\":\"^0.2\"}],\"features\":{}}", "unicase_2.8.1": "{\"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.18": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fst\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"roaring\",\"req\":\"^0.10\"},{\"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\":[]}}", "unicode-segmentation_1.12.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.7\"}],\"features\":{\"no_std\":[]}}", "unicode-truncate_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"itertools\",\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"unicode-segmentation\",\"req\":\"^1\"},{\"name\":\"unicode-width\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "unicode-width_0.1.14": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"std\",\"optional\":true,\"package\":\"rustc-std-workspace-std\",\"req\":\"^1.0\"}],\"features\":{\"cjk\":[],\"default\":[\"cjk\"],\"no_std\":[],\"rustc-dep-of-std\":[\"std\",\"core\",\"compiler_builtins\"]}}", @@ -1189,6 +1282,7 @@ "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\":[]}}", "wasi_0.14.2+wasi-0.2.4": "{\"dependencies\":[{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"bitflags\"],\"name\":\"wit-bindgen-rt\",\"req\":\"^0.39.0\"}],\"features\":{\"default\":[\"std\"],\"rustc-dep-of-std\":[\"compiler_builtins\",\"core\",\"rustc-std-workspace-alloc\"],\"std\":[]}}", + "wasite_0.1.0": "{\"dependencies\":[],\"features\":{}}", "wasm-bindgen-backend_0.2.100": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.0.0\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.100\"}],\"features\":{\"extra-traits\":[\"syn/extra-traits\"]}}", "wasm-bindgen-futures_0.4.50": "{\"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,\"name\":\"js-sys\",\"req\":\"=0.3.77\"},{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.100\"},{\"default_features\":false,\"features\":[\"MessageEvent\",\"Worker\"],\"name\":\"web-sys\",\"req\":\"=0.3.77\",\"target\":\"cfg(target_feature = \\\"atomics\\\")\"}],\"features\":{\"default\":[\"std\"],\"futures-core-03-stream\":[\"futures-core\"],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\",\"web-sys/std\"]}}", "wasm-bindgen-macro-support_0.2.100": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit\",\"visit-mut\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"wasm-bindgen-backend\",\"req\":\"=0.2.100\"},{\"name\":\"wasm-bindgen-shared\",\"req\":\"=0.2.100\"}],\"features\":{\"extra-traits\":[\"syn/extra-traits\"],\"strict-macro\":[]}}", @@ -1210,6 +1304,9 @@ "webpki-roots_1.0.2": "{\"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\"},{\"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.10": "{\"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\"]}}", + "widestring_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"features\":[\"Win32_System_Diagnostics_Debug\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.59\"}],\"features\":{\"alloc\":[],\"debugger_visualizer\":[\"alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "wildcard_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.14\"},{\"kind\":\"dev\",\"name\":\"wildmatch\",\"req\":\"^2.3.4\"}],\"features\":{\"fatal-warnings\":[]}}", "wildmatch_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ntest\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.2\"},{\"kind\":\"dev\",\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", "winapi-i686-pc-windows-gnu_0.4.0": "{\"dependencies\":[],\"features\":{}}", "winapi-util_0.1.9": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_Console\",\"Win32_System_SystemInformation\"],\"name\":\"windows-sys\",\"req\":\">=0.48.0, <=0.59\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -1232,6 +1329,7 @@ "windows-strings_0.1.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"windows-result\",\"req\":\"^0.2.0\"},{\"name\":\"windows-targets\",\"req\":\"^0.52.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "windows-strings_0.4.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"windows-link\",\"req\":\"^0.1.1\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "windows-sys_0.45.0": "{\"dependencies\":[{\"name\":\"windows-targets\",\"req\":\"^0.42.1\",\"target\":\"cfg(not(windows_raw_dylib))\"}],\"features\":{\"Win32\":[],\"Win32_Data\":[\"Win32\"],\"Win32_Data_HtmlHelp\":[\"Win32_Data\"],\"Win32_Data_RightsManagement\":[\"Win32_Data\"],\"Win32_Data_Xml\":[\"Win32_Data\"],\"Win32_Data_Xml_MsXml\":[\"Win32_Data_Xml\"],\"Win32_Data_Xml_XmlLite\":[\"Win32_Data_Xml\"],\"Win32_Devices\":[\"Win32\"],\"Win32_Devices_AllJoyn\":[\"Win32_Devices\"],\"Win32_Devices_BiometricFramework\":[\"Win32_Devices\"],\"Win32_Devices_Bluetooth\":[\"Win32_Devices\"],\"Win32_Devices_Communication\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAccess\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAndDriverInstallation\":[\"Win32_Devices\"],\"Win32_Devices_DeviceQuery\":[\"Win32_Devices\"],\"Win32_Devices_Display\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration_Pnp\":[\"Win32_Devices_Enumeration\"],\"Win32_Devices_Fax\":[\"Win32_Devices\"],\"Win32_Devices_FunctionDiscovery\":[\"Win32_Devices\"],\"Win32_Devices_Geolocation\":[\"Win32_Devices\"],\"Win32_Devices_HumanInterfaceDevice\":[\"Win32_Devices\"],\"Win32_Devices_ImageAcquisition\":[\"Win32_Devices\"],\"Win32_Devices_PortableDevices\":[\"Win32_Devices\"],\"Win32_Devices_Properties\":[\"Win32_Devices\"],\"Win32_Devices_Pwm\":[\"Win32_Devices\"],\"Win32_Devices_Sensors\":[\"Win32_Devices\"],\"Win32_Devices_SerialCommunication\":[\"Win32_Devices\"],\"Win32_Devices_Tapi\":[\"Win32_Devices\"],\"Win32_Devices_Usb\":[\"Win32_Devices\"],\"Win32_Devices_WebServicesOnDevices\":[\"Win32_Devices\"],\"Win32_Foundation\":[\"Win32\"],\"Win32_Gaming\":[\"Win32\"],\"Win32_Globalization\":[\"Win32\"],\"Win32_Graphics\":[\"Win32\"],\"Win32_Graphics_Dwm\":[\"Win32_Graphics\"],\"Win32_Graphics_Gdi\":[\"Win32_Graphics\"],\"Win32_Graphics_Hlsl\":[\"Win32_Graphics\"],\"Win32_Graphics_OpenGL\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing_PrintTicket\":[\"Win32_Graphics_Printing\"],\"Win32_Management\":[\"Win32\"],\"Win32_Management_MobileDeviceManagementRegistration\":[\"Win32_Management\"],\"Win32_Media\":[\"Win32\"],\"Win32_Media_Audio\":[\"Win32_Media\"],\"Win32_Media_Audio_Apo\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_DirectMusic\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_Endpoints\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_XAudio2\":[\"Win32_Media_Audio\"],\"Win32_Media_DeviceManager\":[\"Win32_Media\"],\"Win32_Media_DxMediaObjects\":[\"Win32_Media\"],\"Win32_Media_KernelStreaming\":[\"Win32_Media\"],\"Win32_Media_LibrarySharingServices\":[\"Win32_Media\"],\"Win32_Media_MediaPlayer\":[\"Win32_Media\"],\"Win32_Media_Multimedia\":[\"Win32_Media\"],\"Win32_Media_Speech\":[\"Win32_Media\"],\"Win32_Media_Streaming\":[\"Win32_Media\"],\"Win32_Media_WindowsMediaFormat\":[\"Win32_Media\"],\"Win32_NetworkManagement\":[\"Win32\"],\"Win32_NetworkManagement_Dhcp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Dns\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_InternetConnectionWizard\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_IpHelper\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_MobileBroadband\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Multicast\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Ndis\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetBios\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetManagement\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetShell\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkDiagnosticsFramework\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkPolicyServer\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_P2P\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_QoS\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Rras\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Snmp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WNet\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WebDav\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WiFi\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectNow\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectionManager\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFilteringPlatform\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFirewall\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsNetworkVirtualization\":[\"Win32_NetworkManagement\"],\"Win32_Networking\":[\"Win32\"],\"Win32_Networking_ActiveDirectory\":[\"Win32_Networking\"],\"Win32_Networking_BackgroundIntelligentTransferService\":[\"Win32_Networking\"],\"Win32_Networking_Clustering\":[\"Win32_Networking\"],\"Win32_Networking_HttpServer\":[\"Win32_Networking\"],\"Win32_Networking_Ldap\":[\"Win32_Networking\"],\"Win32_Networking_NetworkListManager\":[\"Win32_Networking\"],\"Win32_Networking_RemoteDifferentialCompression\":[\"Win32_Networking\"],\"Win32_Networking_WebSocket\":[\"Win32_Networking\"],\"Win32_Networking_WinHttp\":[\"Win32_Networking\"],\"Win32_Networking_WinInet\":[\"Win32_Networking\"],\"Win32_Networking_WinSock\":[\"Win32_Networking\"],\"Win32_Networking_WindowsWebServices\":[\"Win32_Networking\"],\"Win32_Security\":[\"Win32\"],\"Win32_Security_AppLocker\":[\"Win32_Security\"],\"Win32_Security_Authentication\":[\"Win32_Security\"],\"Win32_Security_Authentication_Identity\":[\"Win32_Security_Authentication\"],\"Win32_Security_Authentication_Identity_Provider\":[\"Win32_Security_Authentication_Identity\"],\"Win32_Security_Authorization\":[\"Win32_Security\"],\"Win32_Security_Authorization_UI\":[\"Win32_Security_Authorization\"],\"Win32_Security_ConfigurationSnapin\":[\"Win32_Security\"],\"Win32_Security_Credentials\":[\"Win32_Security\"],\"Win32_Security_Cryptography\":[\"Win32_Security\"],\"Win32_Security_Cryptography_Catalog\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Certificates\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Sip\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_UI\":[\"Win32_Security_Cryptography\"],\"Win32_Security_DiagnosticDataQuery\":[\"Win32_Security\"],\"Win32_Security_DirectoryServices\":[\"Win32_Security\"],\"Win32_Security_EnterpriseData\":[\"Win32_Security\"],\"Win32_Security_ExtensibleAuthenticationProtocol\":[\"Win32_Security\"],\"Win32_Security_Isolation\":[\"Win32_Security\"],\"Win32_Security_LicenseProtection\":[\"Win32_Security\"],\"Win32_Security_NetworkAccessProtection\":[\"Win32_Security\"],\"Win32_Security_Tpm\":[\"Win32_Security\"],\"Win32_Security_WinTrust\":[\"Win32_Security\"],\"Win32_Security_WinWlx\":[\"Win32_Security\"],\"Win32_Storage\":[\"Win32\"],\"Win32_Storage_Cabinets\":[\"Win32_Storage\"],\"Win32_Storage_CloudFilters\":[\"Win32_Storage\"],\"Win32_Storage_Compression\":[\"Win32_Storage\"],\"Win32_Storage_DataDeduplication\":[\"Win32_Storage\"],\"Win32_Storage_DistributedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_EnhancedStorage\":[\"Win32_Storage\"],\"Win32_Storage_FileHistory\":[\"Win32_Storage\"],\"Win32_Storage_FileServerResourceManager\":[\"Win32_Storage\"],\"Win32_Storage_FileSystem\":[\"Win32_Storage\"],\"Win32_Storage_Imapi\":[\"Win32_Storage\"],\"Win32_Storage_IndexServer\":[\"Win32_Storage\"],\"Win32_Storage_InstallableFileSystems\":[\"Win32_Storage\"],\"Win32_Storage_IscsiDisc\":[\"Win32_Storage\"],\"Win32_Storage_Jet\":[\"Win32_Storage\"],\"Win32_Storage_OfflineFiles\":[\"Win32_Storage\"],\"Win32_Storage_OperationRecorder\":[\"Win32_Storage\"],\"Win32_Storage_Packaging\":[\"Win32_Storage\"],\"Win32_Storage_Packaging_Appx\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_Packaging_Opc\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_ProjectedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_StructuredStorage\":[\"Win32_Storage\"],\"Win32_Storage_Vhd\":[\"Win32_Storage\"],\"Win32_Storage_VirtualDiskService\":[\"Win32_Storage\"],\"Win32_Storage_Vss\":[\"Win32_Storage\"],\"Win32_Storage_Xps\":[\"Win32_Storage\"],\"Win32_Storage_Xps_Printing\":[\"Win32_Storage_Xps\"],\"Win32_System\":[\"Win32\"],\"Win32_System_AddressBook\":[\"Win32_System\"],\"Win32_System_Antimalware\":[\"Win32_System\"],\"Win32_System_ApplicationInstallationAndServicing\":[\"Win32_System\"],\"Win32_System_ApplicationVerifier\":[\"Win32_System\"],\"Win32_System_AssessmentTool\":[\"Win32_System\"],\"Win32_System_Com\":[\"Win32_System\"],\"Win32_System_Com_CallObj\":[\"Win32_System_Com\"],\"Win32_System_Com_ChannelCredentials\":[\"Win32_System_Com\"],\"Win32_System_Com_Events\":[\"Win32_System_Com\"],\"Win32_System_Com_Marshal\":[\"Win32_System_Com\"],\"Win32_System_Com_StructuredStorage\":[\"Win32_System_Com\"],\"Win32_System_Com_UI\":[\"Win32_System_Com\"],\"Win32_System_Com_Urlmon\":[\"Win32_System_Com\"],\"Win32_System_ComponentServices\":[\"Win32_System\"],\"Win32_System_Console\":[\"Win32_System\"],\"Win32_System_Contacts\":[\"Win32_System\"],\"Win32_System_CorrelationVector\":[\"Win32_System\"],\"Win32_System_DataExchange\":[\"Win32_System\"],\"Win32_System_DeploymentServices\":[\"Win32_System\"],\"Win32_System_DesktopSharing\":[\"Win32_System\"],\"Win32_System_DeveloperLicensing\":[\"Win32_System\"],\"Win32_System_Diagnostics\":[\"Win32_System\"],\"Win32_System_Diagnostics_Ceip\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Etw\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ProcessSnapshotting\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ToolHelp\":[\"Win32_System_Diagnostics\"],\"Win32_System_DistributedTransactionCoordinator\":[\"Win32_System\"],\"Win32_System_Environment\":[\"Win32_System\"],\"Win32_System_ErrorReporting\":[\"Win32_System\"],\"Win32_System_EventCollector\":[\"Win32_System\"],\"Win32_System_EventLog\":[\"Win32_System\"],\"Win32_System_EventNotificationService\":[\"Win32_System\"],\"Win32_System_GroupPolicy\":[\"Win32_System\"],\"Win32_System_HostCompute\":[\"Win32_System\"],\"Win32_System_HostComputeNetwork\":[\"Win32_System\"],\"Win32_System_HostComputeSystem\":[\"Win32_System\"],\"Win32_System_Hypervisor\":[\"Win32_System\"],\"Win32_System_IO\":[\"Win32_System\"],\"Win32_System_Iis\":[\"Win32_System\"],\"Win32_System_Ioctl\":[\"Win32_System\"],\"Win32_System_JobObjects\":[\"Win32_System\"],\"Win32_System_Js\":[\"Win32_System\"],\"Win32_System_Kernel\":[\"Win32_System\"],\"Win32_System_LibraryLoader\":[\"Win32_System\"],\"Win32_System_Mailslots\":[\"Win32_System\"],\"Win32_System_Mapi\":[\"Win32_System\"],\"Win32_System_Memory\":[\"Win32_System\"],\"Win32_System_Memory_NonVolatile\":[\"Win32_System_Memory\"],\"Win32_System_MessageQueuing\":[\"Win32_System\"],\"Win32_System_MixedReality\":[\"Win32_System\"],\"Win32_System_Mmc\":[\"Win32_System\"],\"Win32_System_Ole\":[\"Win32_System\"],\"Win32_System_ParentalControls\":[\"Win32_System\"],\"Win32_System_PasswordManagement\":[\"Win32_System\"],\"Win32_System_Performance\":[\"Win32_System\"],\"Win32_System_Performance_HardwareCounterProfiling\":[\"Win32_System_Performance\"],\"Win32_System_Pipes\":[\"Win32_System\"],\"Win32_System_Power\":[\"Win32_System\"],\"Win32_System_ProcessStatus\":[\"Win32_System\"],\"Win32_System_RealTimeCommunications\":[\"Win32_System\"],\"Win32_System_Recovery\":[\"Win32_System\"],\"Win32_System_Registry\":[\"Win32_System\"],\"Win32_System_RemoteAssistance\":[\"Win32_System\"],\"Win32_System_RemoteDesktop\":[\"Win32_System\"],\"Win32_System_RemoteManagement\":[\"Win32_System\"],\"Win32_System_RestartManager\":[\"Win32_System\"],\"Win32_System_Restore\":[\"Win32_System\"],\"Win32_System_Rpc\":[\"Win32_System\"],\"Win32_System_Search\":[\"Win32_System\"],\"Win32_System_Search_Common\":[\"Win32_System_Search\"],\"Win32_System_SecurityCenter\":[\"Win32_System\"],\"Win32_System_ServerBackup\":[\"Win32_System\"],\"Win32_System_Services\":[\"Win32_System\"],\"Win32_System_SettingsManagementInfrastructure\":[\"Win32_System\"],\"Win32_System_SetupAndMigration\":[\"Win32_System\"],\"Win32_System_Shutdown\":[\"Win32_System\"],\"Win32_System_StationsAndDesktops\":[\"Win32_System\"],\"Win32_System_SubsystemForLinux\":[\"Win32_System\"],\"Win32_System_SystemInformation\":[\"Win32_System\"],\"Win32_System_SystemServices\":[\"Win32_System\"],\"Win32_System_TaskScheduler\":[\"Win32_System\"],\"Win32_System_Threading\":[\"Win32_System\"],\"Win32_System_Time\":[\"Win32_System\"],\"Win32_System_TpmBaseServices\":[\"Win32_System\"],\"Win32_System_UpdateAgent\":[\"Win32_System\"],\"Win32_System_UpdateAssessment\":[\"Win32_System\"],\"Win32_System_UserAccessLogging\":[\"Win32_System\"],\"Win32_System_VirtualDosMachines\":[\"Win32_System\"],\"Win32_System_WindowsProgramming\":[\"Win32_System\"],\"Win32_System_WindowsSync\":[\"Win32_System\"],\"Win32_System_Wmi\":[\"Win32_System\"],\"Win32_UI\":[\"Win32\"],\"Win32_UI_Accessibility\":[\"Win32_UI\"],\"Win32_UI_Animation\":[\"Win32_UI\"],\"Win32_UI_ColorSystem\":[\"Win32_UI\"],\"Win32_UI_Controls\":[\"Win32_UI\"],\"Win32_UI_Controls_Dialogs\":[\"Win32_UI_Controls\"],\"Win32_UI_Controls_RichEdit\":[\"Win32_UI_Controls\"],\"Win32_UI_HiDpi\":[\"Win32_UI\"],\"Win32_UI_Input\":[\"Win32_UI\"],\"Win32_UI_Input_Ime\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Ink\":[\"Win32_UI_Input\"],\"Win32_UI_Input_KeyboardAndMouse\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Pointer\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Radial\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Touch\":[\"Win32_UI_Input\"],\"Win32_UI_Input_XboxController\":[\"Win32_UI_Input\"],\"Win32_UI_InteractionContext\":[\"Win32_UI\"],\"Win32_UI_LegacyWindowsEnvironmentFeatures\":[\"Win32_UI\"],\"Win32_UI_Magnification\":[\"Win32_UI\"],\"Win32_UI_Notifications\":[\"Win32_UI\"],\"Win32_UI_Ribbon\":[\"Win32_UI\"],\"Win32_UI_Shell\":[\"Win32_UI\"],\"Win32_UI_Shell_Common\":[\"Win32_UI_Shell\"],\"Win32_UI_Shell_PropertiesSystem\":[\"Win32_UI_Shell\"],\"Win32_UI_TabletPC\":[\"Win32_UI\"],\"Win32_UI_TextServices\":[\"Win32_UI\"],\"Win32_UI_WindowsAndMessaging\":[\"Win32_UI\"],\"Win32_UI_Wpf\":[\"Win32_UI\"],\"default\":[]}}", + "windows-sys_0.48.0": "{\"dependencies\":[{\"name\":\"windows-targets\",\"req\":\"^0.48.0\"}],\"features\":{\"Wdk\":[],\"Wdk_System\":[\"Wdk\"],\"Wdk_System_OfflineRegistry\":[\"Wdk_System\"],\"Win32\":[],\"Win32_Data\":[\"Win32\"],\"Win32_Data_HtmlHelp\":[\"Win32_Data\"],\"Win32_Data_RightsManagement\":[\"Win32_Data\"],\"Win32_Data_Xml\":[\"Win32_Data\"],\"Win32_Data_Xml_MsXml\":[\"Win32_Data_Xml\"],\"Win32_Data_Xml_XmlLite\":[\"Win32_Data_Xml\"],\"Win32_Devices\":[\"Win32\"],\"Win32_Devices_AllJoyn\":[\"Win32_Devices\"],\"Win32_Devices_BiometricFramework\":[\"Win32_Devices\"],\"Win32_Devices_Bluetooth\":[\"Win32_Devices\"],\"Win32_Devices_Communication\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAccess\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAndDriverInstallation\":[\"Win32_Devices\"],\"Win32_Devices_DeviceQuery\":[\"Win32_Devices\"],\"Win32_Devices_Display\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration_Pnp\":[\"Win32_Devices_Enumeration\"],\"Win32_Devices_Fax\":[\"Win32_Devices\"],\"Win32_Devices_FunctionDiscovery\":[\"Win32_Devices\"],\"Win32_Devices_Geolocation\":[\"Win32_Devices\"],\"Win32_Devices_HumanInterfaceDevice\":[\"Win32_Devices\"],\"Win32_Devices_ImageAcquisition\":[\"Win32_Devices\"],\"Win32_Devices_PortableDevices\":[\"Win32_Devices\"],\"Win32_Devices_Properties\":[\"Win32_Devices\"],\"Win32_Devices_Pwm\":[\"Win32_Devices\"],\"Win32_Devices_Sensors\":[\"Win32_Devices\"],\"Win32_Devices_SerialCommunication\":[\"Win32_Devices\"],\"Win32_Devices_Tapi\":[\"Win32_Devices\"],\"Win32_Devices_Usb\":[\"Win32_Devices\"],\"Win32_Devices_WebServicesOnDevices\":[\"Win32_Devices\"],\"Win32_Foundation\":[\"Win32\"],\"Win32_Gaming\":[\"Win32\"],\"Win32_Globalization\":[\"Win32\"],\"Win32_Graphics\":[\"Win32\"],\"Win32_Graphics_Dwm\":[\"Win32_Graphics\"],\"Win32_Graphics_Gdi\":[\"Win32_Graphics\"],\"Win32_Graphics_Hlsl\":[\"Win32_Graphics\"],\"Win32_Graphics_OpenGL\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing_PrintTicket\":[\"Win32_Graphics_Printing\"],\"Win32_Management\":[\"Win32\"],\"Win32_Management_MobileDeviceManagementRegistration\":[\"Win32_Management\"],\"Win32_Media\":[\"Win32\"],\"Win32_Media_Audio\":[\"Win32_Media\"],\"Win32_Media_Audio_Apo\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_DirectMusic\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_Endpoints\":[\"Win32_Media_Audio\"],\"Win32_Media_Audio_XAudio2\":[\"Win32_Media_Audio\"],\"Win32_Media_DeviceManager\":[\"Win32_Media\"],\"Win32_Media_DxMediaObjects\":[\"Win32_Media\"],\"Win32_Media_KernelStreaming\":[\"Win32_Media\"],\"Win32_Media_LibrarySharingServices\":[\"Win32_Media\"],\"Win32_Media_MediaPlayer\":[\"Win32_Media\"],\"Win32_Media_Multimedia\":[\"Win32_Media\"],\"Win32_Media_Speech\":[\"Win32_Media\"],\"Win32_Media_Streaming\":[\"Win32_Media\"],\"Win32_Media_WindowsMediaFormat\":[\"Win32_Media\"],\"Win32_NetworkManagement\":[\"Win32\"],\"Win32_NetworkManagement_Dhcp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Dns\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_InternetConnectionWizard\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_IpHelper\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_MobileBroadband\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Multicast\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Ndis\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetBios\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetManagement\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetShell\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkDiagnosticsFramework\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkPolicyServer\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_P2P\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_QoS\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Rras\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Snmp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WNet\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WebDav\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WiFi\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectNow\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectionManager\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFilteringPlatform\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFirewall\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsNetworkVirtualization\":[\"Win32_NetworkManagement\"],\"Win32_Networking\":[\"Win32\"],\"Win32_Networking_ActiveDirectory\":[\"Win32_Networking\"],\"Win32_Networking_BackgroundIntelligentTransferService\":[\"Win32_Networking\"],\"Win32_Networking_Clustering\":[\"Win32_Networking\"],\"Win32_Networking_HttpServer\":[\"Win32_Networking\"],\"Win32_Networking_Ldap\":[\"Win32_Networking\"],\"Win32_Networking_NetworkListManager\":[\"Win32_Networking\"],\"Win32_Networking_RemoteDifferentialCompression\":[\"Win32_Networking\"],\"Win32_Networking_WebSocket\":[\"Win32_Networking\"],\"Win32_Networking_WinHttp\":[\"Win32_Networking\"],\"Win32_Networking_WinInet\":[\"Win32_Networking\"],\"Win32_Networking_WinSock\":[\"Win32_Networking\"],\"Win32_Networking_WindowsWebServices\":[\"Win32_Networking\"],\"Win32_Security\":[\"Win32\"],\"Win32_Security_AppLocker\":[\"Win32_Security\"],\"Win32_Security_Authentication\":[\"Win32_Security\"],\"Win32_Security_Authentication_Identity\":[\"Win32_Security_Authentication\"],\"Win32_Security_Authentication_Identity_Provider\":[\"Win32_Security_Authentication_Identity\"],\"Win32_Security_Authorization\":[\"Win32_Security\"],\"Win32_Security_Authorization_UI\":[\"Win32_Security_Authorization\"],\"Win32_Security_ConfigurationSnapin\":[\"Win32_Security\"],\"Win32_Security_Credentials\":[\"Win32_Security\"],\"Win32_Security_Cryptography\":[\"Win32_Security\"],\"Win32_Security_Cryptography_Catalog\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Certificates\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Sip\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_UI\":[\"Win32_Security_Cryptography\"],\"Win32_Security_DiagnosticDataQuery\":[\"Win32_Security\"],\"Win32_Security_DirectoryServices\":[\"Win32_Security\"],\"Win32_Security_EnterpriseData\":[\"Win32_Security\"],\"Win32_Security_ExtensibleAuthenticationProtocol\":[\"Win32_Security\"],\"Win32_Security_Isolation\":[\"Win32_Security\"],\"Win32_Security_LicenseProtection\":[\"Win32_Security\"],\"Win32_Security_NetworkAccessProtection\":[\"Win32_Security\"],\"Win32_Security_Tpm\":[\"Win32_Security\"],\"Win32_Security_WinTrust\":[\"Win32_Security\"],\"Win32_Security_WinWlx\":[\"Win32_Security\"],\"Win32_Storage\":[\"Win32\"],\"Win32_Storage_Cabinets\":[\"Win32_Storage\"],\"Win32_Storage_CloudFilters\":[\"Win32_Storage\"],\"Win32_Storage_Compression\":[\"Win32_Storage\"],\"Win32_Storage_DataDeduplication\":[\"Win32_Storage\"],\"Win32_Storage_DistributedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_EnhancedStorage\":[\"Win32_Storage\"],\"Win32_Storage_FileHistory\":[\"Win32_Storage\"],\"Win32_Storage_FileServerResourceManager\":[\"Win32_Storage\"],\"Win32_Storage_FileSystem\":[\"Win32_Storage\"],\"Win32_Storage_Imapi\":[\"Win32_Storage\"],\"Win32_Storage_IndexServer\":[\"Win32_Storage\"],\"Win32_Storage_InstallableFileSystems\":[\"Win32_Storage\"],\"Win32_Storage_IscsiDisc\":[\"Win32_Storage\"],\"Win32_Storage_Jet\":[\"Win32_Storage\"],\"Win32_Storage_OfflineFiles\":[\"Win32_Storage\"],\"Win32_Storage_OperationRecorder\":[\"Win32_Storage\"],\"Win32_Storage_Packaging\":[\"Win32_Storage\"],\"Win32_Storage_Packaging_Appx\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_Packaging_Opc\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_ProjectedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_StructuredStorage\":[\"Win32_Storage\"],\"Win32_Storage_Vhd\":[\"Win32_Storage\"],\"Win32_Storage_VirtualDiskService\":[\"Win32_Storage\"],\"Win32_Storage_Vss\":[\"Win32_Storage\"],\"Win32_Storage_Xps\":[\"Win32_Storage\"],\"Win32_Storage_Xps_Printing\":[\"Win32_Storage_Xps\"],\"Win32_System\":[\"Win32\"],\"Win32_System_AddressBook\":[\"Win32_System\"],\"Win32_System_Antimalware\":[\"Win32_System\"],\"Win32_System_ApplicationInstallationAndServicing\":[\"Win32_System\"],\"Win32_System_ApplicationVerifier\":[\"Win32_System\"],\"Win32_System_AssessmentTool\":[\"Win32_System\"],\"Win32_System_ClrHosting\":[\"Win32_System\"],\"Win32_System_Com\":[\"Win32_System\"],\"Win32_System_Com_CallObj\":[\"Win32_System_Com\"],\"Win32_System_Com_ChannelCredentials\":[\"Win32_System_Com\"],\"Win32_System_Com_Events\":[\"Win32_System_Com\"],\"Win32_System_Com_Marshal\":[\"Win32_System_Com\"],\"Win32_System_Com_StructuredStorage\":[\"Win32_System_Com\"],\"Win32_System_Com_UI\":[\"Win32_System_Com\"],\"Win32_System_Com_Urlmon\":[\"Win32_System_Com\"],\"Win32_System_ComponentServices\":[\"Win32_System\"],\"Win32_System_Console\":[\"Win32_System\"],\"Win32_System_Contacts\":[\"Win32_System\"],\"Win32_System_CorrelationVector\":[\"Win32_System\"],\"Win32_System_DataExchange\":[\"Win32_System\"],\"Win32_System_DeploymentServices\":[\"Win32_System\"],\"Win32_System_DesktopSharing\":[\"Win32_System\"],\"Win32_System_DeveloperLicensing\":[\"Win32_System\"],\"Win32_System_Diagnostics\":[\"Win32_System\"],\"Win32_System_Diagnostics_Ceip\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ClrProfiling\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug_ActiveScript\":[\"Win32_System_Diagnostics_Debug\"],\"Win32_System_Diagnostics_Debug_Extensions\":[\"Win32_System_Diagnostics_Debug\"],\"Win32_System_Diagnostics_Etw\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ProcessSnapshotting\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ToolHelp\":[\"Win32_System_Diagnostics\"],\"Win32_System_DistributedTransactionCoordinator\":[\"Win32_System\"],\"Win32_System_Environment\":[\"Win32_System\"],\"Win32_System_ErrorReporting\":[\"Win32_System\"],\"Win32_System_EventCollector\":[\"Win32_System\"],\"Win32_System_EventLog\":[\"Win32_System\"],\"Win32_System_EventNotificationService\":[\"Win32_System\"],\"Win32_System_GroupPolicy\":[\"Win32_System\"],\"Win32_System_HostCompute\":[\"Win32_System\"],\"Win32_System_HostComputeNetwork\":[\"Win32_System\"],\"Win32_System_HostComputeSystem\":[\"Win32_System\"],\"Win32_System_Hypervisor\":[\"Win32_System\"],\"Win32_System_IO\":[\"Win32_System\"],\"Win32_System_Iis\":[\"Win32_System\"],\"Win32_System_Ioctl\":[\"Win32_System\"],\"Win32_System_JobObjects\":[\"Win32_System\"],\"Win32_System_Js\":[\"Win32_System\"],\"Win32_System_Kernel\":[\"Win32_System\"],\"Win32_System_LibraryLoader\":[\"Win32_System\"],\"Win32_System_Mailslots\":[\"Win32_System\"],\"Win32_System_Mapi\":[\"Win32_System\"],\"Win32_System_Memory\":[\"Win32_System\"],\"Win32_System_Memory_NonVolatile\":[\"Win32_System_Memory\"],\"Win32_System_MessageQueuing\":[\"Win32_System\"],\"Win32_System_MixedReality\":[\"Win32_System\"],\"Win32_System_Mmc\":[\"Win32_System\"],\"Win32_System_Ole\":[\"Win32_System\"],\"Win32_System_ParentalControls\":[\"Win32_System\"],\"Win32_System_PasswordManagement\":[\"Win32_System\"],\"Win32_System_Performance\":[\"Win32_System\"],\"Win32_System_Performance_HardwareCounterProfiling\":[\"Win32_System_Performance\"],\"Win32_System_Pipes\":[\"Win32_System\"],\"Win32_System_Power\":[\"Win32_System\"],\"Win32_System_ProcessStatus\":[\"Win32_System\"],\"Win32_System_RealTimeCommunications\":[\"Win32_System\"],\"Win32_System_Recovery\":[\"Win32_System\"],\"Win32_System_Registry\":[\"Win32_System\"],\"Win32_System_RemoteAssistance\":[\"Win32_System\"],\"Win32_System_RemoteDesktop\":[\"Win32_System\"],\"Win32_System_RemoteManagement\":[\"Win32_System\"],\"Win32_System_RestartManager\":[\"Win32_System\"],\"Win32_System_Restore\":[\"Win32_System\"],\"Win32_System_Rpc\":[\"Win32_System\"],\"Win32_System_Search\":[\"Win32_System\"],\"Win32_System_Search_Common\":[\"Win32_System_Search\"],\"Win32_System_SecurityCenter\":[\"Win32_System\"],\"Win32_System_ServerBackup\":[\"Win32_System\"],\"Win32_System_Services\":[\"Win32_System\"],\"Win32_System_SettingsManagementInfrastructure\":[\"Win32_System\"],\"Win32_System_SetupAndMigration\":[\"Win32_System\"],\"Win32_System_Shutdown\":[\"Win32_System\"],\"Win32_System_StationsAndDesktops\":[\"Win32_System\"],\"Win32_System_SubsystemForLinux\":[\"Win32_System\"],\"Win32_System_SystemInformation\":[\"Win32_System\"],\"Win32_System_SystemServices\":[\"Win32_System\"],\"Win32_System_TaskScheduler\":[\"Win32_System\"],\"Win32_System_Threading\":[\"Win32_System\"],\"Win32_System_Time\":[\"Win32_System\"],\"Win32_System_TpmBaseServices\":[\"Win32_System\"],\"Win32_System_UpdateAgent\":[\"Win32_System\"],\"Win32_System_UpdateAssessment\":[\"Win32_System\"],\"Win32_System_UserAccessLogging\":[\"Win32_System\"],\"Win32_System_VirtualDosMachines\":[\"Win32_System\"],\"Win32_System_WindowsProgramming\":[\"Win32_System\"],\"Win32_System_WindowsSync\":[\"Win32_System\"],\"Win32_System_Wmi\":[\"Win32_System\"],\"Win32_UI\":[\"Win32\"],\"Win32_UI_Accessibility\":[\"Win32_UI\"],\"Win32_UI_Animation\":[\"Win32_UI\"],\"Win32_UI_ColorSystem\":[\"Win32_UI\"],\"Win32_UI_Controls\":[\"Win32_UI\"],\"Win32_UI_Controls_Dialogs\":[\"Win32_UI_Controls\"],\"Win32_UI_Controls_RichEdit\":[\"Win32_UI_Controls\"],\"Win32_UI_HiDpi\":[\"Win32_UI\"],\"Win32_UI_Input\":[\"Win32_UI\"],\"Win32_UI_Input_Ime\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Ink\":[\"Win32_UI_Input\"],\"Win32_UI_Input_KeyboardAndMouse\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Pointer\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Radial\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Touch\":[\"Win32_UI_Input\"],\"Win32_UI_Input_XboxController\":[\"Win32_UI_Input\"],\"Win32_UI_InteractionContext\":[\"Win32_UI\"],\"Win32_UI_LegacyWindowsEnvironmentFeatures\":[\"Win32_UI\"],\"Win32_UI_Magnification\":[\"Win32_UI\"],\"Win32_UI_Notifications\":[\"Win32_UI\"],\"Win32_UI_Ribbon\":[\"Win32_UI\"],\"Win32_UI_Shell\":[\"Win32_UI\"],\"Win32_UI_Shell_Common\":[\"Win32_UI_Shell\"],\"Win32_UI_Shell_PropertiesSystem\":[\"Win32_UI_Shell\"],\"Win32_UI_TabletPC\":[\"Win32_UI\"],\"Win32_UI_TextServices\":[\"Win32_UI\"],\"Win32_UI_WindowsAndMessaging\":[\"Win32_UI\"],\"Win32_UI_Wpf\":[\"Win32_UI\"],\"Win32_Web\":[\"Win32\"],\"Win32_Web_InternetExplorer\":[\"Win32_Web\"],\"default\":[]}}", "windows-sys_0.52.0": "{\"dependencies\":[{\"name\":\"windows-targets\",\"req\":\"^0.52.0\"}],\"features\":{\"Wdk\":[],\"Wdk_Foundation\":[\"Wdk\"],\"Wdk_Graphics\":[\"Wdk\"],\"Wdk_Graphics_Direct3D\":[\"Wdk_Graphics\"],\"Wdk_Storage\":[\"Wdk\"],\"Wdk_Storage_FileSystem\":[\"Wdk_Storage\"],\"Wdk_Storage_FileSystem_Minifilters\":[\"Wdk_Storage_FileSystem\"],\"Wdk_System\":[\"Wdk\"],\"Wdk_System_IO\":[\"Wdk_System\"],\"Wdk_System_OfflineRegistry\":[\"Wdk_System\"],\"Wdk_System_Registry\":[\"Wdk_System\"],\"Wdk_System_SystemInformation\":[\"Wdk_System\"],\"Wdk_System_SystemServices\":[\"Wdk_System\"],\"Wdk_System_Threading\":[\"Wdk_System\"],\"Win32\":[],\"Win32_Data\":[\"Win32\"],\"Win32_Data_HtmlHelp\":[\"Win32_Data\"],\"Win32_Data_RightsManagement\":[\"Win32_Data\"],\"Win32_Devices\":[\"Win32\"],\"Win32_Devices_AllJoyn\":[\"Win32_Devices\"],\"Win32_Devices_BiometricFramework\":[\"Win32_Devices\"],\"Win32_Devices_Bluetooth\":[\"Win32_Devices\"],\"Win32_Devices_Communication\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAndDriverInstallation\":[\"Win32_Devices\"],\"Win32_Devices_DeviceQuery\":[\"Win32_Devices\"],\"Win32_Devices_Display\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration_Pnp\":[\"Win32_Devices_Enumeration\"],\"Win32_Devices_Fax\":[\"Win32_Devices\"],\"Win32_Devices_HumanInterfaceDevice\":[\"Win32_Devices\"],\"Win32_Devices_PortableDevices\":[\"Win32_Devices\"],\"Win32_Devices_Properties\":[\"Win32_Devices\"],\"Win32_Devices_Pwm\":[\"Win32_Devices\"],\"Win32_Devices_Sensors\":[\"Win32_Devices\"],\"Win32_Devices_SerialCommunication\":[\"Win32_Devices\"],\"Win32_Devices_Tapi\":[\"Win32_Devices\"],\"Win32_Devices_Usb\":[\"Win32_Devices\"],\"Win32_Devices_WebServicesOnDevices\":[\"Win32_Devices\"],\"Win32_Foundation\":[\"Win32\"],\"Win32_Gaming\":[\"Win32\"],\"Win32_Globalization\":[\"Win32\"],\"Win32_Graphics\":[\"Win32\"],\"Win32_Graphics_Dwm\":[\"Win32_Graphics\"],\"Win32_Graphics_Gdi\":[\"Win32_Graphics\"],\"Win32_Graphics_GdiPlus\":[\"Win32_Graphics\"],\"Win32_Graphics_Hlsl\":[\"Win32_Graphics\"],\"Win32_Graphics_OpenGL\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing_PrintTicket\":[\"Win32_Graphics_Printing\"],\"Win32_Management\":[\"Win32\"],\"Win32_Management_MobileDeviceManagementRegistration\":[\"Win32_Management\"],\"Win32_Media\":[\"Win32\"],\"Win32_Media_Audio\":[\"Win32_Media\"],\"Win32_Media_DxMediaObjects\":[\"Win32_Media\"],\"Win32_Media_KernelStreaming\":[\"Win32_Media\"],\"Win32_Media_Multimedia\":[\"Win32_Media\"],\"Win32_Media_Streaming\":[\"Win32_Media\"],\"Win32_Media_WindowsMediaFormat\":[\"Win32_Media\"],\"Win32_NetworkManagement\":[\"Win32\"],\"Win32_NetworkManagement_Dhcp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Dns\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_InternetConnectionWizard\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_IpHelper\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Multicast\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Ndis\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetBios\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetManagement\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetShell\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkDiagnosticsFramework\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_P2P\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_QoS\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Rras\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Snmp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WNet\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WebDav\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WiFi\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectionManager\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFilteringPlatform\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFirewall\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsNetworkVirtualization\":[\"Win32_NetworkManagement\"],\"Win32_Networking\":[\"Win32\"],\"Win32_Networking_ActiveDirectory\":[\"Win32_Networking\"],\"Win32_Networking_Clustering\":[\"Win32_Networking\"],\"Win32_Networking_HttpServer\":[\"Win32_Networking\"],\"Win32_Networking_Ldap\":[\"Win32_Networking\"],\"Win32_Networking_WebSocket\":[\"Win32_Networking\"],\"Win32_Networking_WinHttp\":[\"Win32_Networking\"],\"Win32_Networking_WinInet\":[\"Win32_Networking\"],\"Win32_Networking_WinSock\":[\"Win32_Networking\"],\"Win32_Networking_WindowsWebServices\":[\"Win32_Networking\"],\"Win32_Security\":[\"Win32\"],\"Win32_Security_AppLocker\":[\"Win32_Security\"],\"Win32_Security_Authentication\":[\"Win32_Security\"],\"Win32_Security_Authentication_Identity\":[\"Win32_Security_Authentication\"],\"Win32_Security_Authorization\":[\"Win32_Security\"],\"Win32_Security_Credentials\":[\"Win32_Security\"],\"Win32_Security_Cryptography\":[\"Win32_Security\"],\"Win32_Security_Cryptography_Catalog\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Certificates\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Sip\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_UI\":[\"Win32_Security_Cryptography\"],\"Win32_Security_DiagnosticDataQuery\":[\"Win32_Security\"],\"Win32_Security_DirectoryServices\":[\"Win32_Security\"],\"Win32_Security_EnterpriseData\":[\"Win32_Security\"],\"Win32_Security_ExtensibleAuthenticationProtocol\":[\"Win32_Security\"],\"Win32_Security_Isolation\":[\"Win32_Security\"],\"Win32_Security_LicenseProtection\":[\"Win32_Security\"],\"Win32_Security_NetworkAccessProtection\":[\"Win32_Security\"],\"Win32_Security_WinTrust\":[\"Win32_Security\"],\"Win32_Security_WinWlx\":[\"Win32_Security\"],\"Win32_Storage\":[\"Win32\"],\"Win32_Storage_Cabinets\":[\"Win32_Storage\"],\"Win32_Storage_CloudFilters\":[\"Win32_Storage\"],\"Win32_Storage_Compression\":[\"Win32_Storage\"],\"Win32_Storage_DistributedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_FileHistory\":[\"Win32_Storage\"],\"Win32_Storage_FileSystem\":[\"Win32_Storage\"],\"Win32_Storage_Imapi\":[\"Win32_Storage\"],\"Win32_Storage_IndexServer\":[\"Win32_Storage\"],\"Win32_Storage_InstallableFileSystems\":[\"Win32_Storage\"],\"Win32_Storage_IscsiDisc\":[\"Win32_Storage\"],\"Win32_Storage_Jet\":[\"Win32_Storage\"],\"Win32_Storage_Nvme\":[\"Win32_Storage\"],\"Win32_Storage_OfflineFiles\":[\"Win32_Storage\"],\"Win32_Storage_OperationRecorder\":[\"Win32_Storage\"],\"Win32_Storage_Packaging\":[\"Win32_Storage\"],\"Win32_Storage_Packaging_Appx\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_ProjectedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_StructuredStorage\":[\"Win32_Storage\"],\"Win32_Storage_Vhd\":[\"Win32_Storage\"],\"Win32_Storage_Xps\":[\"Win32_Storage\"],\"Win32_System\":[\"Win32\"],\"Win32_System_AddressBook\":[\"Win32_System\"],\"Win32_System_Antimalware\":[\"Win32_System\"],\"Win32_System_ApplicationInstallationAndServicing\":[\"Win32_System\"],\"Win32_System_ApplicationVerifier\":[\"Win32_System\"],\"Win32_System_ClrHosting\":[\"Win32_System\"],\"Win32_System_Com\":[\"Win32_System\"],\"Win32_System_Com_Marshal\":[\"Win32_System_Com\"],\"Win32_System_Com_StructuredStorage\":[\"Win32_System_Com\"],\"Win32_System_Com_Urlmon\":[\"Win32_System_Com\"],\"Win32_System_ComponentServices\":[\"Win32_System\"],\"Win32_System_Console\":[\"Win32_System\"],\"Win32_System_CorrelationVector\":[\"Win32_System\"],\"Win32_System_DataExchange\":[\"Win32_System\"],\"Win32_System_DeploymentServices\":[\"Win32_System\"],\"Win32_System_DeveloperLicensing\":[\"Win32_System\"],\"Win32_System_Diagnostics\":[\"Win32_System\"],\"Win32_System_Diagnostics_Ceip\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug_Extensions\":[\"Win32_System_Diagnostics_Debug\"],\"Win32_System_Diagnostics_Etw\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ProcessSnapshotting\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ToolHelp\":[\"Win32_System_Diagnostics\"],\"Win32_System_DistributedTransactionCoordinator\":[\"Win32_System\"],\"Win32_System_Environment\":[\"Win32_System\"],\"Win32_System_ErrorReporting\":[\"Win32_System\"],\"Win32_System_EventCollector\":[\"Win32_System\"],\"Win32_System_EventLog\":[\"Win32_System\"],\"Win32_System_EventNotificationService\":[\"Win32_System\"],\"Win32_System_GroupPolicy\":[\"Win32_System\"],\"Win32_System_HostCompute\":[\"Win32_System\"],\"Win32_System_HostComputeNetwork\":[\"Win32_System\"],\"Win32_System_HostComputeSystem\":[\"Win32_System\"],\"Win32_System_Hypervisor\":[\"Win32_System\"],\"Win32_System_IO\":[\"Win32_System\"],\"Win32_System_Iis\":[\"Win32_System\"],\"Win32_System_Ioctl\":[\"Win32_System\"],\"Win32_System_JobObjects\":[\"Win32_System\"],\"Win32_System_Js\":[\"Win32_System\"],\"Win32_System_Kernel\":[\"Win32_System\"],\"Win32_System_LibraryLoader\":[\"Win32_System\"],\"Win32_System_Mailslots\":[\"Win32_System\"],\"Win32_System_Mapi\":[\"Win32_System\"],\"Win32_System_Memory\":[\"Win32_System\"],\"Win32_System_Memory_NonVolatile\":[\"Win32_System_Memory\"],\"Win32_System_MessageQueuing\":[\"Win32_System\"],\"Win32_System_MixedReality\":[\"Win32_System\"],\"Win32_System_Ole\":[\"Win32_System\"],\"Win32_System_PasswordManagement\":[\"Win32_System\"],\"Win32_System_Performance\":[\"Win32_System\"],\"Win32_System_Performance_HardwareCounterProfiling\":[\"Win32_System_Performance\"],\"Win32_System_Pipes\":[\"Win32_System\"],\"Win32_System_Power\":[\"Win32_System\"],\"Win32_System_ProcessStatus\":[\"Win32_System\"],\"Win32_System_Recovery\":[\"Win32_System\"],\"Win32_System_Registry\":[\"Win32_System\"],\"Win32_System_RemoteDesktop\":[\"Win32_System\"],\"Win32_System_RemoteManagement\":[\"Win32_System\"],\"Win32_System_RestartManager\":[\"Win32_System\"],\"Win32_System_Restore\":[\"Win32_System\"],\"Win32_System_Rpc\":[\"Win32_System\"],\"Win32_System_Search\":[\"Win32_System\"],\"Win32_System_Search_Common\":[\"Win32_System_Search\"],\"Win32_System_SecurityCenter\":[\"Win32_System\"],\"Win32_System_Services\":[\"Win32_System\"],\"Win32_System_SetupAndMigration\":[\"Win32_System\"],\"Win32_System_Shutdown\":[\"Win32_System\"],\"Win32_System_StationsAndDesktops\":[\"Win32_System\"],\"Win32_System_SubsystemForLinux\":[\"Win32_System\"],\"Win32_System_SystemInformation\":[\"Win32_System\"],\"Win32_System_SystemServices\":[\"Win32_System\"],\"Win32_System_Threading\":[\"Win32_System\"],\"Win32_System_Time\":[\"Win32_System\"],\"Win32_System_TpmBaseServices\":[\"Win32_System\"],\"Win32_System_UserAccessLogging\":[\"Win32_System\"],\"Win32_System_Variant\":[\"Win32_System\"],\"Win32_System_VirtualDosMachines\":[\"Win32_System\"],\"Win32_System_WindowsProgramming\":[\"Win32_System\"],\"Win32_System_Wmi\":[\"Win32_System\"],\"Win32_UI\":[\"Win32\"],\"Win32_UI_Accessibility\":[\"Win32_UI\"],\"Win32_UI_ColorSystem\":[\"Win32_UI\"],\"Win32_UI_Controls\":[\"Win32_UI\"],\"Win32_UI_Controls_Dialogs\":[\"Win32_UI_Controls\"],\"Win32_UI_HiDpi\":[\"Win32_UI\"],\"Win32_UI_Input\":[\"Win32_UI\"],\"Win32_UI_Input_Ime\":[\"Win32_UI_Input\"],\"Win32_UI_Input_KeyboardAndMouse\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Pointer\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Touch\":[\"Win32_UI_Input\"],\"Win32_UI_Input_XboxController\":[\"Win32_UI_Input\"],\"Win32_UI_InteractionContext\":[\"Win32_UI\"],\"Win32_UI_Magnification\":[\"Win32_UI\"],\"Win32_UI_Shell\":[\"Win32_UI\"],\"Win32_UI_Shell_PropertiesSystem\":[\"Win32_UI_Shell\"],\"Win32_UI_TabletPC\":[\"Win32_UI\"],\"Win32_UI_TextServices\":[\"Win32_UI\"],\"Win32_UI_WindowsAndMessaging\":[\"Win32_UI\"],\"Win32_Web\":[\"Win32\"],\"Win32_Web_InternetExplorer\":[\"Win32_Web\"],\"default\":[],\"docs\":[]}}", "windows-sys_0.59.0": "{\"dependencies\":[{\"name\":\"windows-targets\",\"req\":\"^0.52.6\"}],\"features\":{\"Wdk\":[\"Win32_Foundation\"],\"Wdk_Devices\":[\"Wdk\"],\"Wdk_Devices_Bluetooth\":[\"Wdk_Devices\"],\"Wdk_Devices_HumanInterfaceDevice\":[\"Wdk_Devices\"],\"Wdk_Foundation\":[\"Wdk\"],\"Wdk_Graphics\":[\"Wdk\"],\"Wdk_Graphics_Direct3D\":[\"Wdk_Graphics\"],\"Wdk_NetworkManagement\":[\"Wdk\"],\"Wdk_NetworkManagement_Ndis\":[\"Wdk_NetworkManagement\"],\"Wdk_NetworkManagement_WindowsFilteringPlatform\":[\"Wdk_NetworkManagement\"],\"Wdk_Storage\":[\"Wdk\"],\"Wdk_Storage_FileSystem\":[\"Wdk_Storage\"],\"Wdk_Storage_FileSystem_Minifilters\":[\"Wdk_Storage_FileSystem\"],\"Wdk_System\":[\"Wdk\"],\"Wdk_System_IO\":[\"Wdk_System\"],\"Wdk_System_Memory\":[\"Wdk_System\"],\"Wdk_System_OfflineRegistry\":[\"Wdk_System\"],\"Wdk_System_Registry\":[\"Wdk_System\"],\"Wdk_System_SystemInformation\":[\"Wdk_System\"],\"Wdk_System_SystemServices\":[\"Wdk_System\"],\"Wdk_System_Threading\":[\"Wdk_System\"],\"Win32\":[\"Win32_Foundation\"],\"Win32_Data\":[\"Win32\"],\"Win32_Data_HtmlHelp\":[\"Win32_Data\"],\"Win32_Data_RightsManagement\":[\"Win32_Data\"],\"Win32_Devices\":[\"Win32\"],\"Win32_Devices_AllJoyn\":[\"Win32_Devices\"],\"Win32_Devices_BiometricFramework\":[\"Win32_Devices\"],\"Win32_Devices_Bluetooth\":[\"Win32_Devices\"],\"Win32_Devices_Communication\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAndDriverInstallation\":[\"Win32_Devices\"],\"Win32_Devices_DeviceQuery\":[\"Win32_Devices\"],\"Win32_Devices_Display\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration_Pnp\":[\"Win32_Devices_Enumeration\"],\"Win32_Devices_Fax\":[\"Win32_Devices\"],\"Win32_Devices_HumanInterfaceDevice\":[\"Win32_Devices\"],\"Win32_Devices_PortableDevices\":[\"Win32_Devices\"],\"Win32_Devices_Properties\":[\"Win32_Devices\"],\"Win32_Devices_Pwm\":[\"Win32_Devices\"],\"Win32_Devices_Sensors\":[\"Win32_Devices\"],\"Win32_Devices_SerialCommunication\":[\"Win32_Devices\"],\"Win32_Devices_Tapi\":[\"Win32_Devices\"],\"Win32_Devices_Usb\":[\"Win32_Devices\"],\"Win32_Devices_WebServicesOnDevices\":[\"Win32_Devices\"],\"Win32_Foundation\":[\"Win32\"],\"Win32_Gaming\":[\"Win32\"],\"Win32_Globalization\":[\"Win32\"],\"Win32_Graphics\":[\"Win32\"],\"Win32_Graphics_Dwm\":[\"Win32_Graphics\"],\"Win32_Graphics_Gdi\":[\"Win32_Graphics\"],\"Win32_Graphics_GdiPlus\":[\"Win32_Graphics\"],\"Win32_Graphics_Hlsl\":[\"Win32_Graphics\"],\"Win32_Graphics_OpenGL\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing_PrintTicket\":[\"Win32_Graphics_Printing\"],\"Win32_Management\":[\"Win32\"],\"Win32_Management_MobileDeviceManagementRegistration\":[\"Win32_Management\"],\"Win32_Media\":[\"Win32\"],\"Win32_Media_Audio\":[\"Win32_Media\"],\"Win32_Media_DxMediaObjects\":[\"Win32_Media\"],\"Win32_Media_KernelStreaming\":[\"Win32_Media\"],\"Win32_Media_Multimedia\":[\"Win32_Media\"],\"Win32_Media_Streaming\":[\"Win32_Media\"],\"Win32_Media_WindowsMediaFormat\":[\"Win32_Media\"],\"Win32_NetworkManagement\":[\"Win32\"],\"Win32_NetworkManagement_Dhcp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Dns\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_InternetConnectionWizard\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_IpHelper\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Multicast\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Ndis\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetBios\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetManagement\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetShell\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkDiagnosticsFramework\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_P2P\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_QoS\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Rras\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Snmp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WNet\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WebDav\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WiFi\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectionManager\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFilteringPlatform\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFirewall\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsNetworkVirtualization\":[\"Win32_NetworkManagement\"],\"Win32_Networking\":[\"Win32\"],\"Win32_Networking_ActiveDirectory\":[\"Win32_Networking\"],\"Win32_Networking_Clustering\":[\"Win32_Networking\"],\"Win32_Networking_HttpServer\":[\"Win32_Networking\"],\"Win32_Networking_Ldap\":[\"Win32_Networking\"],\"Win32_Networking_WebSocket\":[\"Win32_Networking\"],\"Win32_Networking_WinHttp\":[\"Win32_Networking\"],\"Win32_Networking_WinInet\":[\"Win32_Networking\"],\"Win32_Networking_WinSock\":[\"Win32_Networking\"],\"Win32_Networking_WindowsWebServices\":[\"Win32_Networking\"],\"Win32_Security\":[\"Win32\"],\"Win32_Security_AppLocker\":[\"Win32_Security\"],\"Win32_Security_Authentication\":[\"Win32_Security\"],\"Win32_Security_Authentication_Identity\":[\"Win32_Security_Authentication\"],\"Win32_Security_Authorization\":[\"Win32_Security\"],\"Win32_Security_Credentials\":[\"Win32_Security\"],\"Win32_Security_Cryptography\":[\"Win32_Security\"],\"Win32_Security_Cryptography_Catalog\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Certificates\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Sip\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_UI\":[\"Win32_Security_Cryptography\"],\"Win32_Security_DiagnosticDataQuery\":[\"Win32_Security\"],\"Win32_Security_DirectoryServices\":[\"Win32_Security\"],\"Win32_Security_EnterpriseData\":[\"Win32_Security\"],\"Win32_Security_ExtensibleAuthenticationProtocol\":[\"Win32_Security\"],\"Win32_Security_Isolation\":[\"Win32_Security\"],\"Win32_Security_LicenseProtection\":[\"Win32_Security\"],\"Win32_Security_NetworkAccessProtection\":[\"Win32_Security\"],\"Win32_Security_WinTrust\":[\"Win32_Security\"],\"Win32_Security_WinWlx\":[\"Win32_Security\"],\"Win32_Storage\":[\"Win32\"],\"Win32_Storage_Cabinets\":[\"Win32_Storage\"],\"Win32_Storage_CloudFilters\":[\"Win32_Storage\"],\"Win32_Storage_Compression\":[\"Win32_Storage\"],\"Win32_Storage_DistributedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_FileHistory\":[\"Win32_Storage\"],\"Win32_Storage_FileSystem\":[\"Win32_Storage\"],\"Win32_Storage_Imapi\":[\"Win32_Storage\"],\"Win32_Storage_IndexServer\":[\"Win32_Storage\"],\"Win32_Storage_InstallableFileSystems\":[\"Win32_Storage\"],\"Win32_Storage_IscsiDisc\":[\"Win32_Storage\"],\"Win32_Storage_Jet\":[\"Win32_Storage\"],\"Win32_Storage_Nvme\":[\"Win32_Storage\"],\"Win32_Storage_OfflineFiles\":[\"Win32_Storage\"],\"Win32_Storage_OperationRecorder\":[\"Win32_Storage\"],\"Win32_Storage_Packaging\":[\"Win32_Storage\"],\"Win32_Storage_Packaging_Appx\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_ProjectedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_StructuredStorage\":[\"Win32_Storage\"],\"Win32_Storage_Vhd\":[\"Win32_Storage\"],\"Win32_Storage_Xps\":[\"Win32_Storage\"],\"Win32_System\":[\"Win32\"],\"Win32_System_AddressBook\":[\"Win32_System\"],\"Win32_System_Antimalware\":[\"Win32_System\"],\"Win32_System_ApplicationInstallationAndServicing\":[\"Win32_System\"],\"Win32_System_ApplicationVerifier\":[\"Win32_System\"],\"Win32_System_ClrHosting\":[\"Win32_System\"],\"Win32_System_Com\":[\"Win32_System\"],\"Win32_System_Com_Marshal\":[\"Win32_System_Com\"],\"Win32_System_Com_StructuredStorage\":[\"Win32_System_Com\"],\"Win32_System_Com_Urlmon\":[\"Win32_System_Com\"],\"Win32_System_ComponentServices\":[\"Win32_System\"],\"Win32_System_Console\":[\"Win32_System\"],\"Win32_System_CorrelationVector\":[\"Win32_System\"],\"Win32_System_DataExchange\":[\"Win32_System\"],\"Win32_System_DeploymentServices\":[\"Win32_System\"],\"Win32_System_DeveloperLicensing\":[\"Win32_System\"],\"Win32_System_Diagnostics\":[\"Win32_System\"],\"Win32_System_Diagnostics_Ceip\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug_Extensions\":[\"Win32_System_Diagnostics_Debug\"],\"Win32_System_Diagnostics_Etw\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ProcessSnapshotting\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ToolHelp\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_TraceLogging\":[\"Win32_System_Diagnostics\"],\"Win32_System_DistributedTransactionCoordinator\":[\"Win32_System\"],\"Win32_System_Environment\":[\"Win32_System\"],\"Win32_System_ErrorReporting\":[\"Win32_System\"],\"Win32_System_EventCollector\":[\"Win32_System\"],\"Win32_System_EventLog\":[\"Win32_System\"],\"Win32_System_EventNotificationService\":[\"Win32_System\"],\"Win32_System_GroupPolicy\":[\"Win32_System\"],\"Win32_System_HostCompute\":[\"Win32_System\"],\"Win32_System_HostComputeNetwork\":[\"Win32_System\"],\"Win32_System_HostComputeSystem\":[\"Win32_System\"],\"Win32_System_Hypervisor\":[\"Win32_System\"],\"Win32_System_IO\":[\"Win32_System\"],\"Win32_System_Iis\":[\"Win32_System\"],\"Win32_System_Ioctl\":[\"Win32_System\"],\"Win32_System_JobObjects\":[\"Win32_System\"],\"Win32_System_Js\":[\"Win32_System\"],\"Win32_System_Kernel\":[\"Win32_System\"],\"Win32_System_LibraryLoader\":[\"Win32_System\"],\"Win32_System_Mailslots\":[\"Win32_System\"],\"Win32_System_Mapi\":[\"Win32_System\"],\"Win32_System_Memory\":[\"Win32_System\"],\"Win32_System_Memory_NonVolatile\":[\"Win32_System_Memory\"],\"Win32_System_MessageQueuing\":[\"Win32_System\"],\"Win32_System_MixedReality\":[\"Win32_System\"],\"Win32_System_Ole\":[\"Win32_System\"],\"Win32_System_PasswordManagement\":[\"Win32_System\"],\"Win32_System_Performance\":[\"Win32_System\"],\"Win32_System_Performance_HardwareCounterProfiling\":[\"Win32_System_Performance\"],\"Win32_System_Pipes\":[\"Win32_System\"],\"Win32_System_Power\":[\"Win32_System\"],\"Win32_System_ProcessStatus\":[\"Win32_System\"],\"Win32_System_Recovery\":[\"Win32_System\"],\"Win32_System_Registry\":[\"Win32_System\"],\"Win32_System_RemoteDesktop\":[\"Win32_System\"],\"Win32_System_RemoteManagement\":[\"Win32_System\"],\"Win32_System_RestartManager\":[\"Win32_System\"],\"Win32_System_Restore\":[\"Win32_System\"],\"Win32_System_Rpc\":[\"Win32_System\"],\"Win32_System_Search\":[\"Win32_System\"],\"Win32_System_Search_Common\":[\"Win32_System_Search\"],\"Win32_System_SecurityCenter\":[\"Win32_System\"],\"Win32_System_Services\":[\"Win32_System\"],\"Win32_System_SetupAndMigration\":[\"Win32_System\"],\"Win32_System_Shutdown\":[\"Win32_System\"],\"Win32_System_StationsAndDesktops\":[\"Win32_System\"],\"Win32_System_SubsystemForLinux\":[\"Win32_System\"],\"Win32_System_SystemInformation\":[\"Win32_System\"],\"Win32_System_SystemServices\":[\"Win32_System\"],\"Win32_System_Threading\":[\"Win32_System\"],\"Win32_System_Time\":[\"Win32_System\"],\"Win32_System_TpmBaseServices\":[\"Win32_System\"],\"Win32_System_UserAccessLogging\":[\"Win32_System\"],\"Win32_System_Variant\":[\"Win32_System\"],\"Win32_System_VirtualDosMachines\":[\"Win32_System\"],\"Win32_System_WindowsProgramming\":[\"Win32_System\"],\"Win32_System_Wmi\":[\"Win32_System\"],\"Win32_UI\":[\"Win32\"],\"Win32_UI_Accessibility\":[\"Win32_UI\"],\"Win32_UI_ColorSystem\":[\"Win32_UI\"],\"Win32_UI_Controls\":[\"Win32_UI\"],\"Win32_UI_Controls_Dialogs\":[\"Win32_UI_Controls\"],\"Win32_UI_HiDpi\":[\"Win32_UI\"],\"Win32_UI_Input\":[\"Win32_UI\"],\"Win32_UI_Input_Ime\":[\"Win32_UI_Input\"],\"Win32_UI_Input_KeyboardAndMouse\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Pointer\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Touch\":[\"Win32_UI_Input\"],\"Win32_UI_Input_XboxController\":[\"Win32_UI_Input\"],\"Win32_UI_InteractionContext\":[\"Win32_UI\"],\"Win32_UI_Magnification\":[\"Win32_UI\"],\"Win32_UI_Shell\":[\"Win32_UI\"],\"Win32_UI_Shell_Common\":[\"Win32_UI_Shell\"],\"Win32_UI_Shell_PropertiesSystem\":[\"Win32_UI_Shell\"],\"Win32_UI_TabletPC\":[\"Win32_UI\"],\"Win32_UI_TextServices\":[\"Win32_UI\"],\"Win32_UI_WindowsAndMessaging\":[\"Win32_UI\"],\"Win32_Web\":[\"Win32\"],\"Win32_Web_InternetExplorer\":[\"Win32_Web\"],\"default\":[],\"docs\":[]}}", "windows-sys_0.60.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"windows-targets\",\"req\":\"^0.53.2\"}],\"features\":{\"Wdk\":[\"Win32_Foundation\"],\"Wdk_Devices\":[\"Wdk\"],\"Wdk_Devices_Bluetooth\":[\"Wdk_Devices\"],\"Wdk_Devices_HumanInterfaceDevice\":[\"Wdk_Devices\"],\"Wdk_Foundation\":[\"Wdk\"],\"Wdk_Graphics\":[\"Wdk\"],\"Wdk_Graphics_Direct3D\":[\"Wdk_Graphics\"],\"Wdk_NetworkManagement\":[\"Wdk\"],\"Wdk_NetworkManagement_Ndis\":[\"Wdk_NetworkManagement\"],\"Wdk_NetworkManagement_WindowsFilteringPlatform\":[\"Wdk_NetworkManagement\"],\"Wdk_Storage\":[\"Wdk\"],\"Wdk_Storage_FileSystem\":[\"Wdk_Storage\"],\"Wdk_Storage_FileSystem_Minifilters\":[\"Wdk_Storage_FileSystem\"],\"Wdk_System\":[\"Wdk\"],\"Wdk_System_IO\":[\"Wdk_System\"],\"Wdk_System_Memory\":[\"Wdk_System\"],\"Wdk_System_OfflineRegistry\":[\"Wdk_System\"],\"Wdk_System_Registry\":[\"Wdk_System\"],\"Wdk_System_SystemInformation\":[\"Wdk_System\"],\"Wdk_System_SystemServices\":[\"Wdk_System\"],\"Wdk_System_Threading\":[\"Wdk_System\"],\"Win32\":[\"Win32_Foundation\"],\"Win32_Data\":[\"Win32\"],\"Win32_Data_HtmlHelp\":[\"Win32_Data\"],\"Win32_Data_RightsManagement\":[\"Win32_Data\"],\"Win32_Devices\":[\"Win32\"],\"Win32_Devices_AllJoyn\":[\"Win32_Devices\"],\"Win32_Devices_Beep\":[\"Win32_Devices\"],\"Win32_Devices_BiometricFramework\":[\"Win32_Devices\"],\"Win32_Devices_Bluetooth\":[\"Win32_Devices\"],\"Win32_Devices_Cdrom\":[\"Win32_Devices\"],\"Win32_Devices_Communication\":[\"Win32_Devices\"],\"Win32_Devices_DeviceAndDriverInstallation\":[\"Win32_Devices\"],\"Win32_Devices_DeviceQuery\":[\"Win32_Devices\"],\"Win32_Devices_Display\":[\"Win32_Devices\"],\"Win32_Devices_Dvd\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration\":[\"Win32_Devices\"],\"Win32_Devices_Enumeration_Pnp\":[\"Win32_Devices_Enumeration\"],\"Win32_Devices_Fax\":[\"Win32_Devices\"],\"Win32_Devices_HumanInterfaceDevice\":[\"Win32_Devices\"],\"Win32_Devices_Nfc\":[\"Win32_Devices\"],\"Win32_Devices_Nfp\":[\"Win32_Devices\"],\"Win32_Devices_PortableDevices\":[\"Win32_Devices\"],\"Win32_Devices_Properties\":[\"Win32_Devices\"],\"Win32_Devices_Pwm\":[\"Win32_Devices\"],\"Win32_Devices_Sensors\":[\"Win32_Devices\"],\"Win32_Devices_SerialCommunication\":[\"Win32_Devices\"],\"Win32_Devices_Tapi\":[\"Win32_Devices\"],\"Win32_Devices_Usb\":[\"Win32_Devices\"],\"Win32_Devices_WebServicesOnDevices\":[\"Win32_Devices\"],\"Win32_Foundation\":[\"Win32\"],\"Win32_Gaming\":[\"Win32\"],\"Win32_Globalization\":[\"Win32\"],\"Win32_Graphics\":[\"Win32\"],\"Win32_Graphics_Dwm\":[\"Win32_Graphics\"],\"Win32_Graphics_Gdi\":[\"Win32_Graphics\"],\"Win32_Graphics_GdiPlus\":[\"Win32_Graphics\"],\"Win32_Graphics_Hlsl\":[\"Win32_Graphics\"],\"Win32_Graphics_OpenGL\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing\":[\"Win32_Graphics\"],\"Win32_Graphics_Printing_PrintTicket\":[\"Win32_Graphics_Printing\"],\"Win32_Management\":[\"Win32\"],\"Win32_Management_MobileDeviceManagementRegistration\":[\"Win32_Management\"],\"Win32_Media\":[\"Win32\"],\"Win32_Media_Audio\":[\"Win32_Media\"],\"Win32_Media_DxMediaObjects\":[\"Win32_Media\"],\"Win32_Media_KernelStreaming\":[\"Win32_Media\"],\"Win32_Media_Multimedia\":[\"Win32_Media\"],\"Win32_Media_Streaming\":[\"Win32_Media\"],\"Win32_Media_WindowsMediaFormat\":[\"Win32_Media\"],\"Win32_NetworkManagement\":[\"Win32\"],\"Win32_NetworkManagement_Dhcp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Dns\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_InternetConnectionWizard\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_IpHelper\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Multicast\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Ndis\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetBios\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetManagement\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetShell\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_NetworkDiagnosticsFramework\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_P2P\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_QoS\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Rras\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_Snmp\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WNet\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WebDav\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WiFi\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsConnectionManager\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFilteringPlatform\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsFirewall\":[\"Win32_NetworkManagement\"],\"Win32_NetworkManagement_WindowsNetworkVirtualization\":[\"Win32_NetworkManagement\"],\"Win32_Networking\":[\"Win32\"],\"Win32_Networking_ActiveDirectory\":[\"Win32_Networking\"],\"Win32_Networking_Clustering\":[\"Win32_Networking\"],\"Win32_Networking_HttpServer\":[\"Win32_Networking\"],\"Win32_Networking_Ldap\":[\"Win32_Networking\"],\"Win32_Networking_WebSocket\":[\"Win32_Networking\"],\"Win32_Networking_WinHttp\":[\"Win32_Networking\"],\"Win32_Networking_WinInet\":[\"Win32_Networking\"],\"Win32_Networking_WinSock\":[\"Win32_Networking\"],\"Win32_Networking_WindowsWebServices\":[\"Win32_Networking\"],\"Win32_Security\":[\"Win32\"],\"Win32_Security_AppLocker\":[\"Win32_Security\"],\"Win32_Security_Authentication\":[\"Win32_Security\"],\"Win32_Security_Authentication_Identity\":[\"Win32_Security_Authentication\"],\"Win32_Security_Authorization\":[\"Win32_Security\"],\"Win32_Security_Credentials\":[\"Win32_Security\"],\"Win32_Security_Cryptography\":[\"Win32_Security\"],\"Win32_Security_Cryptography_Catalog\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Certificates\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_Sip\":[\"Win32_Security_Cryptography\"],\"Win32_Security_Cryptography_UI\":[\"Win32_Security_Cryptography\"],\"Win32_Security_DiagnosticDataQuery\":[\"Win32_Security\"],\"Win32_Security_DirectoryServices\":[\"Win32_Security\"],\"Win32_Security_EnterpriseData\":[\"Win32_Security\"],\"Win32_Security_ExtensibleAuthenticationProtocol\":[\"Win32_Security\"],\"Win32_Security_Isolation\":[\"Win32_Security\"],\"Win32_Security_LicenseProtection\":[\"Win32_Security\"],\"Win32_Security_NetworkAccessProtection\":[\"Win32_Security\"],\"Win32_Security_WinTrust\":[\"Win32_Security\"],\"Win32_Security_WinWlx\":[\"Win32_Security\"],\"Win32_Storage\":[\"Win32\"],\"Win32_Storage_Cabinets\":[\"Win32_Storage\"],\"Win32_Storage_CloudFilters\":[\"Win32_Storage\"],\"Win32_Storage_Compression\":[\"Win32_Storage\"],\"Win32_Storage_DistributedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_FileHistory\":[\"Win32_Storage\"],\"Win32_Storage_FileSystem\":[\"Win32_Storage\"],\"Win32_Storage_Imapi\":[\"Win32_Storage\"],\"Win32_Storage_IndexServer\":[\"Win32_Storage\"],\"Win32_Storage_InstallableFileSystems\":[\"Win32_Storage\"],\"Win32_Storage_IscsiDisc\":[\"Win32_Storage\"],\"Win32_Storage_Jet\":[\"Win32_Storage\"],\"Win32_Storage_Nvme\":[\"Win32_Storage\"],\"Win32_Storage_OfflineFiles\":[\"Win32_Storage\"],\"Win32_Storage_OperationRecorder\":[\"Win32_Storage\"],\"Win32_Storage_Packaging\":[\"Win32_Storage\"],\"Win32_Storage_Packaging_Appx\":[\"Win32_Storage_Packaging\"],\"Win32_Storage_ProjectedFileSystem\":[\"Win32_Storage\"],\"Win32_Storage_StructuredStorage\":[\"Win32_Storage\"],\"Win32_Storage_Vhd\":[\"Win32_Storage\"],\"Win32_Storage_Xps\":[\"Win32_Storage\"],\"Win32_System\":[\"Win32\"],\"Win32_System_AddressBook\":[\"Win32_System\"],\"Win32_System_Antimalware\":[\"Win32_System\"],\"Win32_System_ApplicationInstallationAndServicing\":[\"Win32_System\"],\"Win32_System_ApplicationVerifier\":[\"Win32_System\"],\"Win32_System_ClrHosting\":[\"Win32_System\"],\"Win32_System_Com\":[\"Win32_System\"],\"Win32_System_Com_Marshal\":[\"Win32_System_Com\"],\"Win32_System_Com_StructuredStorage\":[\"Win32_System_Com\"],\"Win32_System_Com_Urlmon\":[\"Win32_System_Com\"],\"Win32_System_ComponentServices\":[\"Win32_System\"],\"Win32_System_Console\":[\"Win32_System\"],\"Win32_System_CorrelationVector\":[\"Win32_System\"],\"Win32_System_DataExchange\":[\"Win32_System\"],\"Win32_System_DeploymentServices\":[\"Win32_System\"],\"Win32_System_DeveloperLicensing\":[\"Win32_System\"],\"Win32_System_Diagnostics\":[\"Win32_System\"],\"Win32_System_Diagnostics_Ceip\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_Debug_Extensions\":[\"Win32_System_Diagnostics_Debug\"],\"Win32_System_Diagnostics_Etw\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ProcessSnapshotting\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_ToolHelp\":[\"Win32_System_Diagnostics\"],\"Win32_System_Diagnostics_TraceLogging\":[\"Win32_System_Diagnostics\"],\"Win32_System_DistributedTransactionCoordinator\":[\"Win32_System\"],\"Win32_System_Environment\":[\"Win32_System\"],\"Win32_System_ErrorReporting\":[\"Win32_System\"],\"Win32_System_EventCollector\":[\"Win32_System\"],\"Win32_System_EventLog\":[\"Win32_System\"],\"Win32_System_EventNotificationService\":[\"Win32_System\"],\"Win32_System_GroupPolicy\":[\"Win32_System\"],\"Win32_System_HostCompute\":[\"Win32_System\"],\"Win32_System_HostComputeNetwork\":[\"Win32_System\"],\"Win32_System_HostComputeSystem\":[\"Win32_System\"],\"Win32_System_Hypervisor\":[\"Win32_System\"],\"Win32_System_IO\":[\"Win32_System\"],\"Win32_System_Iis\":[\"Win32_System\"],\"Win32_System_Ioctl\":[\"Win32_System\"],\"Win32_System_JobObjects\":[\"Win32_System\"],\"Win32_System_Js\":[\"Win32_System\"],\"Win32_System_Kernel\":[\"Win32_System\"],\"Win32_System_LibraryLoader\":[\"Win32_System\"],\"Win32_System_Mailslots\":[\"Win32_System\"],\"Win32_System_Mapi\":[\"Win32_System\"],\"Win32_System_Memory\":[\"Win32_System\"],\"Win32_System_Memory_NonVolatile\":[\"Win32_System_Memory\"],\"Win32_System_MessageQueuing\":[\"Win32_System\"],\"Win32_System_MixedReality\":[\"Win32_System\"],\"Win32_System_Ole\":[\"Win32_System\"],\"Win32_System_PasswordManagement\":[\"Win32_System\"],\"Win32_System_Performance\":[\"Win32_System\"],\"Win32_System_Performance_HardwareCounterProfiling\":[\"Win32_System_Performance\"],\"Win32_System_Pipes\":[\"Win32_System\"],\"Win32_System_Power\":[\"Win32_System\"],\"Win32_System_ProcessStatus\":[\"Win32_System\"],\"Win32_System_Recovery\":[\"Win32_System\"],\"Win32_System_Registry\":[\"Win32_System\"],\"Win32_System_RemoteDesktop\":[\"Win32_System\"],\"Win32_System_RemoteManagement\":[\"Win32_System\"],\"Win32_System_RestartManager\":[\"Win32_System\"],\"Win32_System_Restore\":[\"Win32_System\"],\"Win32_System_Rpc\":[\"Win32_System\"],\"Win32_System_Search\":[\"Win32_System\"],\"Win32_System_Search_Common\":[\"Win32_System_Search\"],\"Win32_System_SecurityCenter\":[\"Win32_System\"],\"Win32_System_Services\":[\"Win32_System\"],\"Win32_System_SetupAndMigration\":[\"Win32_System\"],\"Win32_System_Shutdown\":[\"Win32_System\"],\"Win32_System_StationsAndDesktops\":[\"Win32_System\"],\"Win32_System_SubsystemForLinux\":[\"Win32_System\"],\"Win32_System_SystemInformation\":[\"Win32_System\"],\"Win32_System_SystemServices\":[\"Win32_System\"],\"Win32_System_Threading\":[\"Win32_System\"],\"Win32_System_Time\":[\"Win32_System\"],\"Win32_System_TpmBaseServices\":[\"Win32_System\"],\"Win32_System_UserAccessLogging\":[\"Win32_System\"],\"Win32_System_Variant\":[\"Win32_System\"],\"Win32_System_VirtualDosMachines\":[\"Win32_System\"],\"Win32_System_WindowsProgramming\":[\"Win32_System\"],\"Win32_System_Wmi\":[\"Win32_System\"],\"Win32_UI\":[\"Win32\"],\"Win32_UI_Accessibility\":[\"Win32_UI\"],\"Win32_UI_ColorSystem\":[\"Win32_UI\"],\"Win32_UI_Controls\":[\"Win32_UI\"],\"Win32_UI_Controls_Dialogs\":[\"Win32_UI_Controls\"],\"Win32_UI_HiDpi\":[\"Win32_UI\"],\"Win32_UI_Input\":[\"Win32_UI\"],\"Win32_UI_Input_Ime\":[\"Win32_UI_Input\"],\"Win32_UI_Input_KeyboardAndMouse\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Pointer\":[\"Win32_UI_Input\"],\"Win32_UI_Input_Touch\":[\"Win32_UI_Input\"],\"Win32_UI_Input_XboxController\":[\"Win32_UI_Input\"],\"Win32_UI_InteractionContext\":[\"Win32_UI\"],\"Win32_UI_Magnification\":[\"Win32_UI\"],\"Win32_UI_Shell\":[\"Win32_UI\"],\"Win32_UI_Shell_Common\":[\"Win32_UI_Shell\"],\"Win32_UI_Shell_PropertiesSystem\":[\"Win32_UI_Shell\"],\"Win32_UI_TabletPC\":[\"Win32_UI\"],\"Win32_UI_TextServices\":[\"Win32_UI\"],\"Win32_UI_WindowsAndMessaging\":[\"Win32_UI\"],\"Win32_Web\":[\"Win32\"],\"Win32_Web_InternetExplorer\":[\"Win32_Web\"],\"default\":[],\"docs\":[]}}", @@ -1275,6 +1373,7 @@ "windows_x86_64_msvc_0.53.0": "{\"dependencies\":[],\"features\":{}}", "winnow_0.7.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"annotate-snippets\",\"req\":\"^0.11.3\"},{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.3.2\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"kind\":\"dev\",\"name\":\"circular\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"name\":\"is_terminal_polyfill\",\"optional\":true,\"req\":\"^1.48.0\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.5\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"rustc-hash\",\"req\":\"^1.1.0\"},{\"features\":[\"examples\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"term-transcript\",\"req\":\"^0.2.0\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.0\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\",\"dep:is_terminal_polyfill\",\"dep:terminal_size\"],\"default\":[\"std\"],\"simd\":[\"dep:memchr\"],\"std\":[\"alloc\",\"memchr?/std\"],\"unstable-doc\":[\"alloc\",\"std\",\"simd\",\"unstable-recover\"],\"unstable-recover\":[]}}", "winreg_0.10.1": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"~3.0\"},{\"features\":[\"impl-default\",\"impl-debug\",\"minwindef\",\"minwinbase\",\"timezoneapi\",\"winerror\",\"winnt\",\"winreg\",\"handleapi\"],\"name\":\"winapi\",\"req\":\"^0.3.9\"}],\"features\":{\"serialization-serde\":[\"transactions\",\"serde\"],\"transactions\":[\"winapi/ktmw32\"]}}", + "winreg_0.50.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.3\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_bytes\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"~3.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Time\",\"Win32_System_Registry\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_Diagnostics_Debug\"],\"name\":\"windows-sys\",\"req\":\"^0.48.0\"}],\"features\":{\"serialization-serde\":[\"transactions\",\"serde\"],\"transactions\":[]}}", "winres_0.1.12": "{\"dependencies\":[{\"name\":\"toml\",\"req\":\"^0.5\"},{\"features\":[\"winnt\"],\"kind\":\"dev\",\"name\":\"winapi\",\"req\":\"^0.3\"}],\"features\":{}}", "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\":[]}}", diff --git a/PNPM.md b/PNPM.md index 860633c8e1..9f05e39cf8 100644 --- a/PNPM.md +++ b/PNPM.md @@ -15,7 +15,7 @@ This project has been migrated from npm to pnpm to improve dependency management ```bash # Global installation of pnpm -npm install -g pnpm@10.8.1 +npm install -g pnpm@10.28.2 # Or with corepack (available with Node.js 22+) corepack enable @@ -59,12 +59,12 @@ codex/ ## CI/CD -CI/CD workflows have been updated to use pnpm instead of npm. Make sure your CI environments use pnpm 10.8.1 or higher. +CI/CD workflows have been updated to use pnpm instead of npm. Make sure your CI environments use pnpm 10.28.2 or higher. ## Known issues If you encounter issues with pnpm, try the following solutions: 1. Remove the `node_modules` folder and `pnpm-lock.yaml` file, then run `pnpm install` -2. Make sure you're using pnpm 10.8.1 or higher +2. Make sure you're using pnpm 10.28.2 or higher 3. Verify that Node.js 22 or higher is installed diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index adf4f724f8..4e94e53e08 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -361,7 +361,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.52.0", + "windows-sys 0.60.2", "wl-clipboard-rs", "x11rb", ] @@ -602,6 +602,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -739,6 +748,9 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -1077,6 +1089,7 @@ dependencies = [ "codex-chatgpt", "codex-common", "codex-core", + "codex-execpolicy", "codex-feedback", "codex-file-search", "codex-login", @@ -1363,6 +1376,7 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-rmcp-client", + "codex-state", "codex-utils-absolute-path", "codex-utils-cargo-bin", "codex-utils-pty", @@ -1388,6 +1402,7 @@ dependencies = [ "libc", "maplit", "mcp-types", + "multimap", "notify", "once_cell", "openssl-sys", @@ -1558,11 +1573,13 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "crossbeam-channel", "ignore", - "nucleo-matcher", + "nucleo", "pretty_assertions", "serde", "serde_json", + "tempfile", "tokio", ] @@ -1684,6 +1701,7 @@ dependencies = [ "rama-http", "rama-http-backend", "rama-net", + "rama-socks5", "rama-tcp", "rama-tls-boring", "rama-unix", @@ -1756,6 +1774,7 @@ name = "codex-protocol" version = "0.0.0" dependencies = [ "anyhow", + "codex-execpolicy", "codex-git", "codex-utils-absolute-path", "codex-utils-image", @@ -1825,6 +1844,23 @@ dependencies = [ "which", ] +[[package]] +name = "codex-state" +version = "0.0.0" +dependencies = [ + "anyhow", + "chrono", + "codex-otel", + "codex-protocol", + "pretty_assertions", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "codex-stdio-to-uds" version = "0.0.0" @@ -2108,6 +2144,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.35" @@ -2195,6 +2237,7 @@ dependencies = [ "tokio-tungstenite", "walkdir", "wiremock", + "zstd", ] [[package]] @@ -2206,6 +2249,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -2249,6 +2307,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2527,6 +2594,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ + "const-oid", "pem-rfc7468", "zeroize", ] @@ -2625,6 +2693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -2772,6 +2841,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "ena" @@ -2905,7 +2977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2914,6 +2986,17 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.0" @@ -3005,7 +3088,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3082,6 +3165,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "flume" version = "0.12.0" @@ -3230,6 +3324,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -3310,7 +3415,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", + "windows-link 0.2.0", "windows-result 0.3.4", ] @@ -3462,6 +3567,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "heck" version = "0.5.0" @@ -3665,7 +3779,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -4092,7 +4206,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4297,6 +4411,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -4323,6 +4440,12 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.6" @@ -4331,6 +4454,18 @@ checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags 2.10.0", "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -4515,6 +4650,16 @@ dependencies = [ "wiremock", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "md5" version = "0.8.0" @@ -4758,11 +4903,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nucleo" +version = "0.5.0" +source = "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + [[package]] name = "nucleo-matcher" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +source = "git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee" dependencies = [ "memchr", "unicode-segmentation", @@ -4792,6 +4946,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -4845,6 +5015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -5327,6 +5498,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -5670,7 +5862,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -5940,7 +6132,7 @@ checksum = "b28ee9e1e5d39264414b71f5c33e7fbb66b382c3fac456fe0daad39cf5509933" dependencies = [ "ahash", "const_format", - "flume", + "flume 0.12.0", "hex", "ipnet", "itertools 0.14.0", @@ -5960,6 +6152,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "rama-socks5" +version = "0.3.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5468b263516daaf258de32542c1974b7cbe962363ad913dcb669f5d46db0ef3e" +dependencies = [ + "byteorder", + "rama-core", + "rama-net", + "rama-tcp", + "rama-udp", + "rama-utils", + "tokio", +] + [[package]] name = "rama-tcp" version = "0.3.0-alpha.4" @@ -5983,7 +6190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def3d5d06d3ca3a2d2e4376cf93de0555cd9c7960f085bf77be9562f5c9ace8f" dependencies = [ "ahash", - "flume", + "flume 0.12.0", "itertools 0.14.0", "moka", "parking_lot", @@ -5998,6 +6205,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "rama-udp" +version = "0.3.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ed05e0ecac73e084e92a3a8b1fbf16fdae8958c506f0f0eada180a2d99eef4" +dependencies = [ + "rama-core", + "rama-net", + "tokio", + "tokio-util", +] + [[package]] name = "rama-unix" version = "0.3.0-alpha.4" @@ -6125,6 +6344,26 @@ dependencies = [ "ratatui", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.15" @@ -6263,7 +6502,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -6334,6 +6573,26 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.25" @@ -6365,7 +6624,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6378,7 +6637,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -7083,6 +7342,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -7167,6 +7436,218 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.4", + "hashlink", + "indexmap 2.12.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.104", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.104", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume 0.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "sse-stream" version = "0.2.1" @@ -7300,6 +7781,17 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.10.0" @@ -7465,7 +7957,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -8260,6 +8752,12 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -8272,6 +8770,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -8479,6 +8992,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -8678,6 +9197,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -8704,6 +9232,16 @@ dependencies = [ "winsafe", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "widestring" version = "1.2.1" @@ -8747,7 +9285,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index a8ebb9be89..9b0ce2e16d 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -47,6 +47,7 @@ members = [ "utils/string", "codex-client", "codex-api", + "state", ] resolver = "2" @@ -91,6 +92,7 @@ codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-responses-api-proxy = { path = "responses-api-proxy" } codex-rmcp-client = { path = "rmcp-client" } +codex-state = { path = "state" } codex-stdio-to-uds = { path = "stdio-to-uds" } codex-tui = { path = "tui" } codex-utils-absolute-path = { path = "utils/absolute-path" } @@ -126,6 +128,7 @@ clap = "4" clap_complete = "4" color-eyre = "0.6.3" crossterm = "0.28.1" +crossbeam-channel = "0.5.15" ctor = "0.6.3" derive_more = "2" diffy = "0.4.2" @@ -159,7 +162,7 @@ maplit = "1.0.2" mime_guess = "2.0.5" multimap = "0.10.0" notify = "8.2.0" -nucleo-matcher = "0.3.1" +nucleo = { git = "https://github.com/helix-editor/nucleo.git", rev = "4253de9faabb4e5c6d81d946a5e35a90f87347ee" } once_cell = "1.20.2" openssl-sys = "*" opentelemetry = "0.31.0" @@ -198,6 +201,7 @@ semver = "1.0" shlex = "1.3.0" similar = "2.7.0" socket2 = "0.6.1" +sqlx = { version = "0.8.6", default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio-rustls", "sqlite", "time", "uuid"] } starlark = "0.13.0" strum = "0.27.2" strum_macros = "0.27.2" diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index dac093c5c8..eb58595133 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -598,6 +598,7 @@ server_notification_definitions! { ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification), ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification), ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification), + /// Deprecated: Use `ContextCompaction` item type instead. ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification), ConfigWarning => "configWarning" (v2::ConfigWarningNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 8b5d55bc91..18a3079794 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -27,10 +27,12 @@ use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::SessionSource as CoreSessionSource; +use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; use codex_protocol::protocol::SkillInterface as CoreSkillInterface; use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; use codex_protocol::protocol::SkillScope as CoreSkillScope; +use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; use codex_protocol::protocol::TokenUsage as CoreTokenUsage; use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; @@ -1395,11 +1397,14 @@ pub struct SkillMetadata { pub description: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - /// Legacy short_description from SKILL.md. Prefer SKILL.toml interface.short_description. + /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. pub short_description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub interface: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub dependencies: Option, pub path: PathBuf, pub scope: SkillScope, pub enabled: bool, @@ -1423,6 +1428,35 @@ pub struct SkillInterface { pub default_prompt: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillDependencies { + pub tools: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillToolDependency { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub r#type: String, + pub value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub transport: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub command: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub url: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1462,6 +1496,7 @@ impl From for SkillMetadata { description: value.description, short_description: value.short_description, interface: value.interface.map(SkillInterface::from), + dependencies: value.dependencies.map(SkillDependencies::from), path: value.path, scope: value.scope.into(), enabled: true, @@ -1482,6 +1517,31 @@ impl From for SkillInterface { } } +impl From for SkillDependencies { + fn from(value: CoreSkillDependencies) -> Self { + Self { + tools: value + .tools + .into_iter() + .map(SkillToolDependency::from) + .collect(), + } + } +} + +impl From for SkillToolDependency { + fn from(value: CoreSkillToolDependency) -> Self { + Self { + r#type: value.r#type, + value: value.value, + description: value.description, + transport: value.transport, + command: value.command, + url: value.url, + } + } +} + impl From for SkillScope { fn from(value: CoreSkillScope) -> Self { match value { @@ -1969,6 +2029,9 @@ pub enum ThreadItem { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] ExitedReviewMode { id: String, review: String }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ContextCompaction { id: String }, } impl From for ThreadItem { @@ -1997,6 +2060,9 @@ impl From for ThreadItem { id: search.id, query: search.query, }, + CoreTurnItem::ContextCompaction(compaction) => { + ThreadItem::ContextCompaction { id: compaction.id } + } } } } @@ -2359,6 +2425,7 @@ pub struct WindowsWorldWritableWarningNotification { pub failed_scan: bool, } +/// Deprecated: Use `ContextCompaction` item type instead. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 9fa2044058..af2f30c49c 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -56,6 +56,7 @@ axum = { workspace = true, default-features = false, features = [ "tokio", ] } base64 = { workspace = true } +codex-execpolicy = { workspace = true } core_test_support = { workspace = true } mcp-types = { workspace = true } os_info = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index adf8ef0b45..8195bd23cb 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -431,7 +431,8 @@ Today both notifications carry an empty `items` array even when item events were - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. - `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description. - `exitedReviewMode` — `{id, review}` emitted when the reviewer finishes; `review` is the full plain-text review (usually, overall notes plus bullet point findings). -- `compacted` - `{threadId, turnId}` when codex compacts the conversation history. This can happen automatically. +- `contextCompaction` — `{id}` emitted when codex compacts the conversation history. This can happen automatically. +- `compacted` - `{threadId, turnId}` when codex compacts the conversation history. This can happen automatically. **Deprecated:** Use `contextCompaction` instead. All items emit two shared lifecycle events: diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index fec9b2abdb..c70f705a96 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -169,6 +169,8 @@ use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::rollout_date_parts; use codex_core::sandboxing::SandboxPermissions; +use codex_core::state_db::{self}; +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_feedback::CodexFeedback; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; @@ -176,6 +178,7 @@ use codex_login::run_login_server; use codex_protocol::ThreadId; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; @@ -1259,12 +1262,14 @@ impl CodexMessageProcessor { let timeout_ms = params .timeout_ms .and_then(|timeout_ms| u64::try_from(timeout_ms).ok()); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); let exec_params = ExecParams { command: params.command, cwd, expiration: timeout_ms.into(), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level, justification: None, arg0: None, }; @@ -1605,6 +1610,7 @@ impl CodexMessageProcessor { } async fn thread_archive(&mut self, request_id: RequestId, params: ThreadArchiveParams) { + // TODO(jif) mostly rewrite this using sqlite after phase 1 let thread_id = match ThreadId::from_string(¶ms.thread_id) { Ok(id) => id, Err(err) => { @@ -1654,6 +1660,7 @@ impl CodexMessageProcessor { } async fn thread_unarchive(&mut self, request_id: RequestId, params: ThreadUnarchiveParams) { + // TODO(jif) mostly rewrite this using sqlite after phase 1 let thread_id = match ThreadId::from_string(¶ms.thread_id) { Ok(id) => id, Err(err) => { @@ -1696,6 +1703,7 @@ impl CodexMessageProcessor { let rollout_path_display = archived_path.display().to_string(); let fallback_provider = self.config.model_provider_id.clone(); + let state_db_ctx = state_db::init_if_enabled(&self.config, None).await; let archived_folder = self .config .codex_home @@ -1774,6 +1782,11 @@ impl CodexMessageProcessor { message: format!("failed to unarchive thread: {err}"), data: None, })?; + if let Some(ctx) = state_db_ctx { + let _ = ctx + .mark_unarchived(thread_id, restored_path.as_path()) + .await; + } let summary = read_summary_from_rollout(restored_path.as_path(), fallback_provider.as_str()) .await @@ -2503,7 +2516,6 @@ impl CodexMessageProcessor { }; let fallback_provider = self.config.model_provider_id.as_str(); - match read_summary_from_rollout(&path, fallback_provider).await { Ok(summary) => { let response = GetConversationSummaryResponse { summary }; @@ -3526,8 +3538,13 @@ impl CodexMessageProcessor { }); } + let mut state_db_ctx = None; + // If the thread is active, request shutdown and wait briefly. if let Some(conversation) = self.thread_manager.remove_thread(&thread_id).await { + if let Some(ctx) = conversation.state_db() { + state_db_ctx = Some(ctx); + } info!("thread {thread_id} was active; shutting down"); // Request shutdown. match conversation.submit(Op::Shutdown).await { @@ -3554,14 +3571,24 @@ impl CodexMessageProcessor { } } + if state_db_ctx.is_none() { + state_db_ctx = state_db::init_if_enabled(&self.config, None).await; + } + // Move the rollout file to archived. - let result: std::io::Result<()> = async { + let result: std::io::Result<()> = async move { let archive_folder = self .config .codex_home .join(codex_core::ARCHIVED_SESSIONS_SUBDIR); tokio::fs::create_dir_all(&archive_folder).await?; - tokio::fs::rename(&canonical_rollout_path, &archive_folder.join(&file_name)).await?; + let archived_path = archive_folder.join(&file_name); + tokio::fs::rename(&canonical_rollout_path, &archived_path).await?; + if let Some(ctx) = state_db_ctx { + let _ = ctx + .mark_archived(thread_id, archived_path.as_path(), Utc::now()) + .await; + } Ok(()) } .await; @@ -3887,6 +3914,7 @@ impl CodexMessageProcessor { cwd: params.cwd, approval_policy: params.approval_policy.map(AskForApproval::to_core), sandbox_policy: params.sandbox_policy.map(|p| p.to_core()), + windows_sandbox_level: None, model: params.model, effort: params.effort.map(Some), summary: params.summary, @@ -4517,6 +4545,22 @@ fn skills_to_info( default_prompt: interface.default_prompt, } }), + dependencies: skill.dependencies.clone().map(|dependencies| { + codex_app_server_protocol::SkillDependencies { + tools: dependencies + .tools + .into_iter() + .map(|tool| codex_app_server_protocol::SkillToolDependency { + r#type: tool.r#type, + value: tool.value, + description: tool.description, + transport: tool.transport, + command: tool.command, + url: tool.url, + }) + .collect(), + } + }), path: skill.path.clone(), scope: skill.scope.into(), enabled, diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs index 9c95e3de34..87fdf39117 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -48,8 +48,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { .await??; let value = resp.result; - // The path separator on Windows affects the score. - let expected_score = if cfg!(windows) { 69 } else { 72 }; + let expected_score = 72; assert_eq!( value, @@ -59,16 +58,9 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { "root": root_path.clone(), "path": "abexy", "file_name": "abexy", - "score": 88, + "score": 84, "indices": [0, 1, 2], }, - { - "root": root_path.clone(), - "path": "abcde", - "file_name": "abcde", - "score": 74, - "indices": [0, 1, 4], - }, { "root": root_path.clone(), "path": sub_abce_rel, @@ -76,6 +68,13 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { "score": expected_score, "indices": [4, 5, 7], }, + { + "root": root_path.clone(), + "path": "abcde", + "file_name": "abcde", + "score": 71, + "indices": [0, 1, 4], + }, ] }) ); diff --git a/codex-rs/app-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs index 0c713de87c..814352a007 100644 --- a/codex-rs/app-server/tests/suite/send_message.rs +++ b/codex-rs/app-server/tests/suite/send_message.rs @@ -11,6 +11,7 @@ use codex_app_server_protocol::NewConversationResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserMessageResponse; +use codex_execpolicy::Policy; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; @@ -358,6 +359,8 @@ fn assert_permissions_message(item: &ResponseItem) { let expected = DeveloperInstructions::from_policy( &SandboxPolicy::DangerFullAccess, AskForApproval::Never, + &Policy::empty(), + false, &PathBuf::from("/tmp"), ) .into_text(); diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 6129c0de2f..af746b37b7 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -2,7 +2,9 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::create_fake_rollout_with_text_elements; use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::rollout_path; use app_test_support::to_response; +use chrono::Utc; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SessionSource; @@ -22,6 +24,8 @@ use codex_protocol::user_input::TextElement; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use std::fs::FileTimes; +use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; use tokio::time::timeout; @@ -147,6 +151,116 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; + let thread_id = rollout.conversation_id.clone(); + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: thread_id.clone(), + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; + + assert_eq!(thread.updated_at, rollout.expected_updated_at); + + let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert_eq!(after_modified, rollout.before_modified); + + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let after_turn_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert!(after_turn_modified > rollout.before_modified); + + Ok(()) +} + +#[tokio::test] +async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: rollout.conversation_id.clone(), + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; + + assert_eq!(thread.updated_at, rollout.expected_updated_at); + + let after_resume_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert_eq!(after_resume_modified, rollout.before_modified); + + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: rollout.conversation_id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let after_turn_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert!(after_turn_modified > rollout.before_modified); + + Ok(()) +} + #[tokio::test] async fn thread_resume_prefers_path_over_thread_id() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -364,3 +478,51 @@ stream_max_retries = 0 ), ) } + +fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()> { + let parsed = chrono::DateTime::parse_from_rfc3339(updated_at_rfc3339)?.with_timezone(&Utc); + let times = FileTimes::new().set_modified(parsed.into()); + std::fs::OpenOptions::new() + .append(true) + .open(path)? + .set_times(times)?; + Ok(()) +} + +struct RolloutFixture { + conversation_id: String, + rollout_file_path: PathBuf, + before_modified: std::time::SystemTime, + expected_updated_at: i64, +} + +fn setup_rollout_fixture(codex_home: &Path, server_uri: &str) -> Result { + create_config_toml(codex_home, server_uri)?; + + let preview = "Saved user message"; + let filename_ts = "2025-01-05T12-00-00"; + let meta_rfc3339 = "2025-01-05T12:00:00Z"; + let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home, + filename_ts, + meta_rfc3339, + preview, + Vec::new(), + Some("mock_provider"), + None, + )?; + let rollout_file_path = rollout_path(codex_home, filename_ts, &conversation_id); + set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; + let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? + .with_timezone(&Utc) + .timestamp(); + + Ok(RolloutFixture { + conversation_id, + rollout_file_path, + before_modified, + expected_updated_at, + }) +} diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index ea3585956b..ec5fd3f61b 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -1,4 +1,5 @@ use crate::types::CodeTaskDetailsResponse; +use crate::types::ConfigFileResponse; use crate::types::CreditStatusDetails; use crate::types::PaginatedListTaskListItem; use crate::types::RateLimitStatusPayload; @@ -244,6 +245,20 @@ impl Client { self.decode_json::(&url, &ct, &body) } + /// Fetch the managed requirements file from codex-backend. + /// + /// `GET /api/codex/config/requirements` (Codex API style) or + /// `GET /wham/config/requirements` (ChatGPT backend-api style). + pub async fn get_config_requirements_file(&self) -> Result { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/config/requirements", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/config/requirements", self.base_url), + }; + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request(req, "GET", &url).await?; + self.decode_json::(&url, &ct, &body) + } + /// Create a new task (user turn) by POSTing to the appropriate backend path /// based on `path_style`. Returns the created task id. pub async fn create_task(&self, request_body: serde_json::Value) -> Result { diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 29fe9f3c6b..de827e9a97 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -4,6 +4,7 @@ pub mod types; pub use client::Client; pub use types::CodeTaskDetailsResponse; pub use types::CodeTaskDetailsResponseExt; +pub use types::ConfigFileResponse; pub use types::PaginatedListTaskListItem; pub use types::TaskListItem; pub use types::TurnAttemptsSiblingTurnsResponse; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index afeb231a18..9deeab7903 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -1,3 +1,4 @@ +pub use codex_backend_openapi_models::models::ConfigFileResponse; pub use codex_backend_openapi_models::models::CreditStatusDetails; pub use codex_backend_openapi_models::models::PaginatedListTaskListItem; pub use codex_backend_openapi_models::models::PlanType; diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 8c1f3e5d39..13cb8cdd86 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -136,7 +136,8 @@ async fn run_command_under_sandbox( if let SandboxType::Windows = sandbox_type { #[cfg(target_os = "windows")] { - use codex_core::features::Feature; + use codex_core::windows_sandbox::WindowsSandboxLevelExt; + use codex_protocol::config_types::WindowsSandboxLevel; use codex_windows_sandbox::run_windows_sandbox_capture; use codex_windows_sandbox::run_windows_sandbox_capture_elevated; @@ -147,8 +148,10 @@ async fn run_command_under_sandbox( let env_map = env.clone(); let command_vec = command.clone(); let base_dir = config.codex_home.clone(); - let use_elevated = config.features.enabled(Feature::WindowsSandbox) - && config.features.enabled(Feature::WindowsSandboxElevated); + let use_elevated = matches!( + WindowsSandboxLevel::from_config(&config), + WindowsSandboxLevel::Elevated + ); // Preflight audit is invoked elsewhere at the appropriate times. let res = tokio::task::spawn_blocking(move || { diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 7cc42c4c49..83de37e027 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -13,11 +13,12 @@ use codex_core::config::find_codex_home; use codex_core::config::load_global_mcp_servers; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; +use codex_core::mcp::auth::McpOAuthLoginSupport; use codex_core::mcp::auth::compute_auth_statuses; +use codex_core::mcp::auth::oauth_login_support; use codex_core::protocol::McpAuthStatus; use codex_rmcp_client::delete_oauth_tokens; use codex_rmcp_client::perform_oauth_login; -use codex_rmcp_client::supports_oauth_login; /// Subcommands: /// - `list` — list configured servers (with `--json`) @@ -260,33 +261,25 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re println!("Added global MCP server '{name}'."); - if let McpServerTransportConfig::StreamableHttp { - url, - bearer_token_env_var: None, - http_headers, - env_http_headers, - } = transport - { - match supports_oauth_login(&url).await { - Ok(true) => { - println!("Detected OAuth support. Starting OAuth flow…"); - perform_oauth_login( - &name, - &url, - config.mcp_oauth_credentials_store_mode, - http_headers.clone(), - env_http_headers.clone(), - &Vec::new(), - config.mcp_oauth_callback_port, - ) - .await?; - println!("Successfully logged in."); - } - Ok(false) => {} - Err(_) => println!( - "MCP server may or may not require login. Run `codex mcp login {name}` to login." - ), + match oauth_login_support(&transport).await { + McpOAuthLoginSupport::Supported(oauth_config) => { + println!("Detected OAuth support. Starting OAuth flow…"); + perform_oauth_login( + &name, + &oauth_config.url, + config.mcp_oauth_credentials_store_mode, + oauth_config.http_headers, + oauth_config.env_http_headers, + &Vec::new(), + config.mcp_oauth_callback_port, + ) + .await?; + println!("Successfully logged in."); } + McpOAuthLoginSupport::Unsupported => {} + McpOAuthLoginSupport::Unknown(_) => println!( + "MCP server may or may not require login. Run `codex mcp login {name}` to login." + ), } Ok(()) diff --git a/codex-rs/codex-backend-openapi-models/src/models/config_file_response.rs b/codex-rs/codex-backend-openapi-models/src/models/config_file_response.rs new file mode 100644 index 0000000000..2e22cb58fe --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/config_file_response.rs @@ -0,0 +1,40 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConfigFileResponse { + #[serde(rename = "contents", skip_serializing_if = "Option::is_none")] + pub contents: Option, + #[serde(rename = "sha256", skip_serializing_if = "Option::is_none")] + pub sha256: Option, + #[serde(rename = "updated_at", skip_serializing_if = "Option::is_none")] + pub updated_at: Option, + #[serde(rename = "updated_by_user_id", skip_serializing_if = "Option::is_none")] + pub updated_by_user_id: Option, +} + +impl ConfigFileResponse { + pub fn new( + contents: Option, + sha256: Option, + updated_at: Option, + updated_by_user_id: Option, + ) -> ConfigFileResponse { + ConfigFileResponse { + contents, + sha256, + updated_at, + updated_by_user_id, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/mod.rs b/codex-rs/codex-backend-openapi-models/src/models/mod.rs index d767154925..7072dede5e 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/mod.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/mod.rs @@ -3,6 +3,10 @@ // Currently export only the types referenced by the workspace // The process for this will change +// Config +pub mod config_file_response; +pub use self::config_file_response::ConfigFileResponse; + // Cloud Tasks pub mod code_task_details_response; pub use self::code_task_details_response::CodeTaskDetailsResponse; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index d35c284a33..3e217eb445 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -37,6 +37,7 @@ codex-keyring-store = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-rmcp-client = { workspace = true } +codex-state = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-pty = { workspace = true } codex-utils-readiness = { workspace = true } @@ -56,6 +57,7 @@ keyring = { workspace = true, features = ["crypto-rust"] } libc = { workspace = true } mcp-types = { workspace = true } notify = { workspace = true } +multimap = { workspace = true } once_cell = { workspace = true } os_info = { workspace = true } rand = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6b48dcb4a9..03e27dceda 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -189,6 +189,9 @@ "remote_models": { "type": "boolean" }, + "request_rule": { + "type": "boolean" + }, "responses_websockets": { "type": "boolean" }, @@ -198,6 +201,12 @@ "shell_tool": { "type": "boolean" }, + "skill_mcp_dependency_install": { + "type": "boolean" + }, + "sqlite": { + "type": "boolean" + }, "steer": { "type": "boolean" }, @@ -441,7 +450,6 @@ "type": "object" }, "Notice": { - "additionalProperties": false, "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", "properties": { "hide_full_access_warning": { @@ -475,6 +483,14 @@ }, "type": "object" }, + "NotificationMethod": { + "enum": [ + "auto", + "osc9", + "bel" + ], + "type": "string" + }, "Notifications": { "anyOf": [ { @@ -983,6 +999,15 @@ "default": null, "description": "Start the TUI in the specified collaboration mode (plan/execute/etc.). Defaults to unset." }, + "notification_method": { + "allOf": [ + { + "$ref": "#/definitions/NotificationMethod" + } + ], + "default": "auto", + "description": "Notification method to use for unfocused terminal notifications. Defaults to `auto`." + }, "notifications": { "allOf": [ { @@ -1182,6 +1207,9 @@ "remote_models": { "type": "boolean" }, + "request_rule": { + "type": "boolean" + }, "responses_websockets": { "type": "boolean" }, @@ -1191,6 +1219,12 @@ "shell_tool": { "type": "boolean" }, + "skill_mcp_dependency_install": { + "type": "boolean" + }, + "sqlite": { + "type": "boolean" + }, "steer": { "type": "boolean" }, diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs index dd05c18470..7a9dda005b 100644 --- a/codex-rs/core/src/agent/role.rs +++ b/codex-rs/core/src/agent/role.rs @@ -44,6 +44,8 @@ pub struct AgentProfile { pub reasoning_effort: Option, /// Whether to force a read-only sandbox policy. pub read_only: bool, + /// Description to include in the tool specs. + pub description: &'static str, } impl AgentRole { @@ -51,7 +53,19 @@ impl AgentRole { pub fn enum_values() -> Vec { ALL_ROLES .iter() - .filter_map(|role| serde_json::to_string(role).ok()) + .filter_map(|role| { + let description = role.profile().description; + serde_json::to_string(role) + .map(|role| { + let description = if !description.is_empty() { + format!(r#", "description": {description}"#) + } else { + String::new() + }; + format!(r#"{{ "name": {role}{description}}}"#) + }) + .ok() + }) .collect() } @@ -66,11 +80,33 @@ impl AgentRole { AgentRole::Worker => AgentProfile { // base_instructions: Some(WORKER_PROMPT), // model: Some(WORKER_MODEL), + description: r#"Use for execution and production work. +Typical tasks: +- Implement part of a feature +- Fix tests or bugs +- Split large refactors into independent chunks +Rules: +- Explicitly assign **ownership** of the task (files / responsibility). +- Always tell workers they are **not alone in the codebase**, and they should ignore edits made by others without touching them"#, ..Default::default() }, AgentRole::Explorer => AgentProfile { model: Some(EXPLORER_MODEL), reasoning_effort: Some(ReasoningEffort::Low), + description: r#"Use for fast codebase understanding and information gathering. +`explorer` are extremely fast agents so use them as much as you can to speed up the resolution of the global task. +Typical tasks: +- Locate usages of a symbol or concept +- Understand how X is handled in Y +- Review a section of code for issues +- Assess impact of a potential change +Rules: +- Be explicit in what you are looking for. A good usage of `explorer` would mean that don't need to read the same code after the explorer send you the result. +- **Always** prefer asking explorers rather than exploring the codebase yourself. +- Spawn multiple explorers in parallel when useful and wait for all results. +- You can ask the `explorer` to return file name, lines, entire code snippets, ... +- Reuse the same explorer when it is relevant. If later in your process you have more questions on some code an explorer already covered, reuse this same explorer to be more efficient. + "#, ..Default::default() }, } diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index 1a47ca60b7..f87e07300d 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -42,6 +42,7 @@ pub(crate) async fn apply_patch( turn_context.approval_policy, &turn_context.sandbox_policy, &turn_context.cwd, + turn_context.windows_sandbox_level, ) { SafetyCheck::AutoApprove { user_explicitly_approved, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 6392b3e70c..f415c3604a 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -655,11 +655,13 @@ fn build_responses_headers( let mut headers = experimental_feature_headers(config); headers.insert( WEB_SEARCH_ELIGIBLE_HEADER, - HeaderValue::from_static(if config.web_search_mode == WebSearchMode::Disabled { - "false" - } else { - "true" - }), + HeaderValue::from_static( + if matches!(config.web_search_mode, Some(WebSearchMode::Disabled)) { + "false" + } else { + "true" + }, + ), ); if let Some(turn_state) = turn_state && let Some(state) = turn_state.get() diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 094ae34f64..30ae35fc9e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -100,6 +100,7 @@ use crate::config::Config; use crate::config::Constrained; use crate::config::ConstraintResult; use crate::config::GhostSnapshotConfig; +use crate::config::resolve_web_search_mode_for_turn; use crate::config::types::McpServerConfig; use crate::config::types::ShellEnvironmentPolicy; use crate::context_manager::ContextManager; @@ -116,6 +117,7 @@ use crate::instructions::UserInstructions; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::auth::compute_auth_statuses; use crate::mcp::effective_mcp_servers; +use crate::mcp::maybe_prompt_and_install_mcp_dependencies; use crate::mcp::with_codex_apps_mcp; use crate::mcp_connection_manager::McpConnectionManager; use crate::model_provider_info::CHAT_WIRE_API_DEPRECATION_SUMMARY; @@ -139,9 +141,11 @@ use crate::protocol::RequestUserInputEvent; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; use crate::protocol::SessionConfiguredEvent; +use crate::protocol::SkillDependencies as ProtocolSkillDependencies; use crate::protocol::SkillErrorInfo; use crate::protocol::SkillInterface as ProtocolSkillInterface; use crate::protocol::SkillMetadata as ProtocolSkillMetadata; +use crate::protocol::SkillToolDependency as ProtocolSkillToolDependency; use crate::protocol::StreamErrorEvent; use crate::protocol::Submission; use crate::protocol::TokenCountEvent; @@ -152,6 +156,7 @@ use crate::protocol::WarningEvent; use crate::rollout::RolloutRecorder; use crate::rollout::RolloutRecorderParams; use crate::rollout::map_session_init_error; +use crate::rollout::metadata; use crate::shell; use crate::shell_snapshot::ShellSnapshot; use crate::skills::SkillError; @@ -159,9 +164,11 @@ use crate::skills::SkillInjections; use crate::skills::SkillMetadata; use crate::skills::SkillsManager; use crate::skills::build_skill_injections; +use crate::skills::collect_explicit_skill_mentions; use crate::state::ActiveTurn; use crate::state::SessionServices; use crate::state::SessionState; +use crate::state_db; use crate::tasks::GhostSnapshotTask; use crate::tasks::ReviewTask; use crate::tasks::SessionTask; @@ -176,15 +183,18 @@ use crate::turn_diff_tracker::TurnDiffTracker; use crate::unified_exec::UnifiedExecProcessManager; use crate::user_notification::UserNotification; use crate::util::backoff; +use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_async_utils::OrCancelExt; use codex_otel::OtelManager; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::render_command_prefix_list; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::InitialHistory; use codex_protocol::user_input::UserInput; @@ -328,6 +338,7 @@ impl Codex { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source, @@ -415,6 +426,10 @@ impl Codex { let state = self.session.state.lock().await; state.session_configuration.thread_config_snapshot() } + + pub(crate) fn state_db(&self) -> Option { + self.session.state_db() + } } /// Context for an initialized model agent @@ -451,6 +466,7 @@ pub(crate) struct TurnContext { pub(crate) personality: Option, pub(crate) approval_policy: AskForApproval, pub(crate) sandbox_policy: SandboxPolicy, + pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) tools_config: ToolsConfig, pub(crate) ghost_snapshot: GhostSnapshotConfig, @@ -502,6 +518,7 @@ pub(crate) struct SessionConfiguration { approval_policy: Constrained, /// How to sandbox commands executed in the system sandbox_policy: Constrained, + windows_sandbox_level: WindowsSandboxLevel, /// Working directory that should be treated as the *root* of the /// session. All relative paths supplied by the model as well as the @@ -550,6 +567,9 @@ impl SessionConfiguration { if let Some(sandbox_policy) = updates.sandbox_policy.clone() { next_configuration.sandbox_policy.set(sandbox_policy)?; } + if let Some(windows_sandbox_level) = updates.windows_sandbox_level { + next_configuration.windows_sandbox_level = windows_sandbox_level; + } if let Some(cwd) = updates.cwd.clone() { next_configuration.cwd = cwd; } @@ -562,6 +582,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) cwd: Option, pub(crate) approval_policy: Option, pub(crate) sandbox_policy: Option, + pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, pub(crate) final_output_json_schema: Option>, @@ -578,6 +599,10 @@ impl Session { session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.model_personality = session_configuration.personality; + per_turn_config.web_search_mode = Some(resolve_web_search_mode_for_turn( + per_turn_config.web_search_mode, + session_configuration.sandbox_policy.get(), + )); per_turn_config.features = config.features.clone(); per_turn_config } @@ -677,6 +702,7 @@ impl Session { personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.value(), sandbox_policy: session_configuration.sandbox_policy.get().clone(), + windows_sandbox_level: session_configuration.windows_sandbox_level, shell_environment_policy: per_turn_config.shell_environment_policy.clone(), tools_config, ghost_snapshot: per_turn_config.ghost_snapshot.clone(), @@ -737,6 +763,13 @@ impl Session { RolloutRecorderParams::resume(resumed_history.rollout_path.clone()), ), }; + let state_builder = match &initial_history { + InitialHistory::Resumed(resumed) => metadata::builder_from_items( + resumed.history.as_slice(), + resumed.rollout_path.as_path(), + ), + InitialHistory::New | InitialHistory::Forked(_) => None, + }; // Kick off independent async setup tasks in parallel to reduce startup latency. // @@ -745,11 +778,17 @@ impl Session { // - load history metadata let rollout_fut = async { if config.ephemeral { - Ok(None) + Ok::<_, anyhow::Error>((None, None)) } else { - RolloutRecorder::new(&config, rollout_params) - .await - .map(Some) + let state_db_ctx = state_db::init_if_enabled(&config, None).await; + let rollout_recorder = RolloutRecorder::new( + &config, + rollout_params, + state_db_ctx.clone(), + state_builder.clone(), + ) + .await?; + Ok((Some(rollout_recorder), state_db_ctx)) } }; @@ -769,14 +808,14 @@ impl Session { // Join all independent futures. let ( - rollout_recorder, + rollout_recorder_and_state_db, (history_log_id, history_entry_count), (auth, mcp_servers, auth_statuses), ) = tokio::join!(rollout_fut, history_meta_fut, auth_and_mcp_fut); - let rollout_recorder = rollout_recorder.map_err(|e| { + let (rollout_recorder, state_db_ctx) = rollout_recorder_and_state_db.map_err(|e| { error!("failed to initialize rollout recorder: {e:#}"); - anyhow::Error::from(e) + e })?; let rollout_path = rollout_recorder .as_ref() @@ -784,19 +823,13 @@ impl Session { let mut post_session_configured_events = Vec::::new(); - for (alias, feature) in config.features.legacy_feature_usages() { - let canonical = feature.key(); - let summary = format!("`{alias}` is deprecated. Use `[features].{canonical}` instead."); - let details = if alias == canonical { - None - } else { - Some(format!( - "Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://github.com/openai/codex/blob/main/docs/config.md#feature-flags for details." - )) - }; + for usage in config.features.legacy_feature_usages() { post_session_configured_events.push(Event { id: INITIAL_SUBMIT_ID.to_owned(), - msg: EventMsg::DeprecationNotice(DeprecationNoticeEvent { summary, details }), + msg: EventMsg::DeprecationNotice(DeprecationNoticeEvent { + summary: usage.summary.clone(), + details: usage.details.clone(), + }), }); } if crate::config::uses_deprecated_instructions_file(&config.config_layer_stack) { @@ -882,6 +915,7 @@ impl Session { skills_manager, file_watcher, agent_control, + state_db: state_db_ctx.clone(), }; let sess = Arc::new(Session { @@ -959,6 +993,10 @@ impl Session { self.tx_event.clone() } + pub(crate) fn state_db(&self) -> Option { + self.services.state_db.clone() + } + /// Ensure all rollout writes are durably flushed. pub(crate) async fn flush_rollout(&self) { let recorder = { @@ -1002,23 +1040,28 @@ impl Session { // Build and record initial items (user instructions + environment context) let items = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &items).await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + } // Ensure initial items are visible to immediate readers (e.g., tests, forks). self.flush_rollout().await; } - InitialHistory::Resumed(_) | InitialHistory::Forked(_) => { - let rollout_items = conversation_history.get_rollout_items(); - let persist = matches!(conversation_history, InitialHistory::Forked(_)); + InitialHistory::Resumed(resumed_history) => { + let rollout_items = resumed_history.history; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = false; + } // If resuming, warn when the last recorded model differs from the current one. - if let InitialHistory::Resumed(_) = conversation_history - && let Some(prev) = rollout_items.iter().rev().find_map(|it| { - if let RolloutItem::TurnContext(ctx) = it { - Some(ctx.model.as_str()) - } else { - None - } - }) - { + if let Some(prev) = rollout_items.iter().rev().find_map(|it| { + if let RolloutItem::TurnContext(ctx) = it { + Some(ctx.model.as_str()) + } else { + None + } + }) { let curr = turn_context.client.get_model(); if prev != curr { warn!( @@ -1053,8 +1096,29 @@ impl Session { state.set_token_info(Some(info)); } + // Defer seeding the session's initial context until the first turn starts so + // turn/start overrides can be merged before we write to the rollout. + self.flush_rollout().await; + } + InitialHistory::Forked(rollout_items) => { + // Always add response items to conversation history + let reconstructed_history = self + .reconstruct_history_from_rollout(&turn_context, &rollout_items) + .await; + if !reconstructed_history.is_empty() { + self.record_into_history(&reconstructed_history, &turn_context) + .await; + } + + // Seed usage info from the recorded rollout so UIs can show token counts + // immediately on resume/fork. + if let Some(info) = Self::last_token_info_from_rollout(&rollout_items) { + let mut state = self.state.lock().await; + state.set_token_info(Some(info)); + } + // If persisting, persist all rollout items as-is (recorder filters) - if persist && !rollout_items.is_empty() { + if !rollout_items.is_empty() { self.persist_rollout_items(&rollout_items).await; } @@ -1062,6 +1126,10 @@ impl Session { let initial_context = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &initial_context) .await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + } // Flush after seeding history and any persisted rollout copy. self.flush_rollout().await; } @@ -1246,6 +1314,8 @@ impl Session { DeveloperInstructions::from_policy( &next.sandbox_policy, next.approval_policy, + self.services.exec_policy.current().as_ref(), + self.features.enabled(Feature::RequestRule), &next.cwd, ) .into(), @@ -1436,6 +1506,44 @@ impl Session { Ok(()) } + async fn turn_context_for_sub_id(&self, sub_id: &str) -> Option> { + let active = self.active_turn.lock().await; + active + .as_ref() + .and_then(|turn| turn.tasks.get(sub_id)) + .map(|task| Arc::clone(&task.turn_context)) + } + + pub(crate) async fn record_execpolicy_amendment_message( + &self, + sub_id: &str, + amendment: &ExecPolicyAmendment, + ) { + let Some(prefixes) = render_command_prefix_list([amendment.command.as_slice()]) else { + warn!("execpolicy amendment for {sub_id} had no command prefix"); + return; + }; + let text = format!("Approved command prefix saved:\n{prefixes}"); + let message: ResponseItem = DeveloperInstructions::new(text.clone()).into(); + + if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await { + self.record_conversation_items(&turn_context, std::slice::from_ref(&message)) + .await; + return; + } + + if self + .inject_response_items(vec![ResponseInputItem::Message { + role: "developer".to_string(), + content: vec![ContentItem::InputText { text }], + }]) + .await + .is_err() + { + warn!("no active turn found to record execpolicy amendment message for {sub_id}"); + } + } + /// Emit an exec approval request event and await the user's decision. /// /// The request is keyed by `sub_id`/`call_id` so matching responses are delivered @@ -1712,6 +1820,21 @@ impl Session { state.replace_history(items); } + pub(crate) async fn seed_initial_context_if_needed(&self, turn_context: &TurnContext) { + { + let mut state = self.state.lock().await; + if state.initial_context_seeded { + return; + } + state.initial_context_seeded = true; + } + + let initial_context = self.build_initial_context(turn_context).await; + self.record_conversation_items(turn_context, &initial_context) + .await; + self.flush_rollout().await; + } + async fn persist_rollout_response_items(&self, items: &[ResponseItem]) { let rollout_items: Vec = items .iter() @@ -1780,6 +1903,8 @@ impl Session { DeveloperInstructions::from_policy( &turn_context.sandbox_policy, turn_context.approval_policy, + self.services.exec_policy.current().as_ref(), + self.features.enabled(Feature::RequestRule), &turn_context.cwd, ) .into(), @@ -1892,6 +2017,19 @@ impl Session { self.send_token_count_event(turn_context).await; } + pub(crate) async fn mcp_dependency_prompted(&self) -> HashSet { + let state = self.state.lock().await; + state.mcp_dependency_prompted() + } + + pub(crate) async fn record_mcp_dependency_prompted(&self, names: I) + where + I: IntoIterator, + { + let mut state = self.state.lock().await; + state.record_mcp_dependency_prompted(names); + } + pub(crate) async fn set_server_reasoning_included(&self, included: bool) { let mut state = self.state.lock().await; state.set_server_reasoning_included(included); @@ -2136,35 +2274,12 @@ impl Session { Arc::clone(&self.services.user_shell) } - async fn refresh_mcp_servers_if_requested(&self, turn_context: &TurnContext) { - let refresh_config = { self.pending_mcp_server_refresh_config.lock().await.take() }; - let Some(refresh_config) = refresh_config else { - return; - }; - - let McpServerRefreshConfig { - mcp_servers, - mcp_oauth_credentials_store_mode, - } = refresh_config; - - let mcp_servers = - match serde_json::from_value::>(mcp_servers) { - Ok(servers) => servers, - Err(err) => { - warn!("failed to parse MCP server refresh config: {err}"); - return; - } - }; - let store_mode = match serde_json::from_value::( - mcp_oauth_credentials_store_mode, - ) { - Ok(mode) => mode, - Err(err) => { - warn!("failed to parse MCP OAuth refresh config: {err}"); - return; - } - }; - + async fn refresh_mcp_servers_inner( + &self, + turn_context: &TurnContext, + mcp_servers: HashMap, + store_mode: OAuthCredentialsStoreMode, + ) { let auth = self.services.auth_manager.auth().await; let config = self.get_config().await; let mcp_servers = with_codex_apps_mcp( @@ -2197,6 +2312,49 @@ impl Session { *manager = refreshed_manager; } + async fn refresh_mcp_servers_if_requested(&self, turn_context: &TurnContext) { + let refresh_config = { self.pending_mcp_server_refresh_config.lock().await.take() }; + let Some(refresh_config) = refresh_config else { + return; + }; + + let McpServerRefreshConfig { + mcp_servers, + mcp_oauth_credentials_store_mode, + } = refresh_config; + + let mcp_servers = + match serde_json::from_value::>(mcp_servers) { + Ok(servers) => servers, + Err(err) => { + warn!("failed to parse MCP server refresh config: {err}"); + return; + } + }; + let store_mode = match serde_json::from_value::( + mcp_oauth_credentials_store_mode, + ) { + Ok(mode) => mode, + Err(err) => { + warn!("failed to parse MCP OAuth refresh config: {err}"); + return; + } + }; + + self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode) + .await; + } + + pub(crate) async fn refresh_mcp_servers_now( + &self, + turn_context: &TurnContext, + mcp_servers: HashMap, + store_mode: OAuthCredentialsStoreMode, + ) { + self.refresh_mcp_servers_inner(turn_context, mcp_servers, store_mode) + .await; + } + async fn mcp_startup_cancellation_token(&self) -> CancellationToken { self.services .mcp_startup_cancellation_token @@ -2241,6 +2399,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv cwd, approval_policy, sandbox_policy, + windows_sandbox_level, model, effort, summary, @@ -2264,6 +2423,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv cwd, approval_policy, sandbox_policy, + windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, personality, @@ -2428,6 +2588,11 @@ mod handlers { return; } + let initial_context_seeded = sess.state.lock().await.initial_context_seeded; + if !initial_context_seeded { + return; + } + let current_context = sess.new_default_turn_with_sub_id(sub_id).await; let update_items = sess.build_settings_update_items( Some(&previous_context), @@ -2476,6 +2641,7 @@ mod handlers { cwd: Some(cwd), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), + windows_sandbox_level: None, collaboration_mode, reasoning_summary: Some(summary), final_output_json_schema: Some(final_output_json_schema), @@ -2516,6 +2682,7 @@ mod handlers { // Attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { + sess.seed_initial_context_if_needed(¤t_context).await; let mut update_items = sess.build_settings_update_items( previous_context.as_ref(), ¤t_context, @@ -2595,18 +2762,26 @@ mod handlers { if let ReviewDecision::ApprovedExecpolicyAmendment { proposed_execpolicy_amendment, } = &decision - && let Err(err) = sess + { + match sess .persist_execpolicy_amendment(proposed_execpolicy_amendment) .await - { - let message = format!("Failed to apply execpolicy amendment: {err}"); - tracing::warn!("{message}"); - let warning = EventMsg::Warning(WarningEvent { message }); - sess.send_event_raw(Event { - id: id.clone(), - msg: warning, - }) - .await; + { + Ok(()) => { + sess.record_execpolicy_amendment_message(&id, proposed_execpolicy_amendment) + .await; + } + Err(err) => { + let message = format!("Failed to apply execpolicy amendment: {err}"); + tracing::warn!("{message}"); + let warning = EventMsg::Warning(WarningEvent { message }); + sess.send_event_raw(Event { + id: id.clone(), + msg: warning, + }) + .await; + } + } } match decision { ReviewDecision::Abort => { @@ -2930,7 +3105,7 @@ async fn spawn_review_thread( let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &review_model_info, features: &review_features, - web_search_mode: review_web_search_mode, + web_search_mode: Some(review_web_search_mode), }); let review_prompt = resolved.prompt.clone(); @@ -2942,7 +3117,7 @@ async fn spawn_review_thread( let mut per_turn_config = (*config).clone(); per_turn_config.model = Some(model.clone()); per_turn_config.features = review_features.clone(); - per_turn_config.web_search_mode = review_web_search_mode; + per_turn_config.web_search_mode = Some(review_web_search_mode); let otel_manager = parent_turn_context .client @@ -2973,6 +3148,7 @@ async fn spawn_review_thread( personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy, sandbox_policy: parent_turn_context.sandbox_policy.clone(), + windows_sandbox_level: parent_turn_context.windows_sandbox_level, shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), cwd: parent_turn_context.cwd.clone(), final_output_json_schema: None, @@ -3021,6 +3197,22 @@ fn skills_to_info( brand_color: interface.brand_color, default_prompt: interface.default_prompt, }), + dependencies: skill.dependencies.clone().map(|dependencies| { + ProtocolSkillDependencies { + tools: dependencies + .tools + .into_iter() + .map(|tool| ProtocolSkillToolDependency { + r#type: tool.r#type, + value: tool.value, + description: tool.description, + transport: tool.transport, + command: tool.command, + url: tool.url, + }) + .collect(), + } + }), path: skill.path.clone(), scope: skill.scope, enabled: !disabled_paths.contains(&skill.path), @@ -3080,11 +3272,23 @@ pub(crate) async fn run_turn( .await, ); + let mentioned_skills = skills_outcome.as_ref().map_or_else(Vec::new, |outcome| { + collect_explicit_skill_mentions(&input, &outcome.skills, &outcome.disabled_paths) + }); + + maybe_prompt_and_install_mcp_dependencies( + sess.as_ref(), + turn_context.as_ref(), + &cancellation_token, + &mentioned_skills, + ) + .await; + let otel_manager = turn_context.client.get_otel_manager(); let SkillInjections { items: skill_items, warnings: skill_warnings, - } = build_skill_injections(&input, skills_outcome.as_ref(), Some(&otel_manager)).await; + } = build_skill_injections(&mentioned_skills, Some(&otel_manager)).await; for message in skill_warnings { sess.send_event(&turn_context, EventMsg::Warning(WarningEvent { message })) @@ -3825,6 +4029,23 @@ mod tests { #[tokio::test] async fn record_initial_history_reconstructs_resumed_transcript() { + let (session, turn_context) = make_session_and_context().await; + let (rollout_items, expected) = sample_rollout(&session, &turn_context).await; + + session + .record_initial_history(InitialHistory::Resumed(ResumedHistory { + conversation_id: ThreadId::default(), + history: rollout_items, + rollout_path: PathBuf::from("/tmp/resume.jsonl"), + })) + .await; + + let history = session.state.lock().await.clone_history(); + assert_eq!(expected, history.raw_items()); + } + + #[tokio::test] + async fn resumed_history_seeds_initial_context_on_first_turn_only() { let (session, turn_context) = make_session_and_context().await; let (rollout_items, mut expected) = sample_rollout(&session, &turn_context).await; @@ -3836,9 +4057,17 @@ mod tests { })) .await; + let history_before_seed = session.state.lock().await.clone_history(); + assert_eq!(expected, history_before_seed.raw_items()); + + session.seed_initial_context_if_needed(&turn_context).await; expected.extend(session.build_initial_context(&turn_context).await); - let history = session.state.lock().await.clone_history(); - assert_eq!(expected, history.raw_items()); + let history_after_seed = session.clone_history().await; + assert_eq!(expected, history_after_seed.raw_items()); + + session.seed_initial_context_if_needed(&turn_context).await; + let history_after_second_seed = session.clone_history().await; + assert_eq!(expected, history_after_second_seed.raw_items()); } #[tokio::test] @@ -4092,6 +4321,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4172,6 +4402,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4436,6 +4667,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4453,7 +4685,8 @@ mod tests { session_configuration.session_source.clone(), ); - let state = SessionState::new(session_configuration.clone()); + let mut state = SessionState::new(session_configuration.clone()); + mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let file_watcher = Arc::new(FileWatcher::noop()); @@ -4473,6 +4706,7 @@ mod tests { skills_manager, file_watcher, agent_control, + state_db: None, }; let turn_context = Session::make_turn_context( @@ -4549,6 +4783,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4566,7 +4801,8 @@ mod tests { session_configuration.session_source.clone(), ); - let state = SessionState::new(session_configuration.clone()); + let mut state = SessionState::new(session_configuration.clone()); + mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let file_watcher = Arc::new(FileWatcher::noop()); @@ -4586,6 +4822,7 @@ mod tests { skills_manager, file_watcher, agent_control, + state_db: None, }; let turn_context = Arc::new(Session::make_turn_context( @@ -4616,6 +4853,10 @@ mod tests { (session, turn_context, rx_event) } + fn mark_state_initial_context_seeded(state: &mut SessionState) { + state.initial_context_seeded = true; + } + #[tokio::test] async fn refresh_mcp_servers_is_deferred_until_next_turn() { let (session, turn_context) = make_session_and_context().await; @@ -5055,6 +5296,7 @@ mod tests { expiration: timeout_ms.into(), env: HashMap::new(), sandbox_permissions, + windows_sandbox_level: turn_context.windows_sandbox_level, justification: Some("test".to_string()), arg0: None, }; @@ -5065,6 +5307,7 @@ mod tests { cwd: params.cwd.clone(), expiration: timeout_ms.into(), env: HashMap::new(), + windows_sandbox_level: turn_context.windows_sandbox_level, justification: params.justification.clone(), arg0: None, }; diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 152679d146..fb8e466d71 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -12,6 +12,8 @@ use codex_protocol::protocol::SessionSource; use std::path::PathBuf; use tokio::sync::watch; +use crate::state_db::StateDbHandle; + #[derive(Clone, Debug)] pub struct ThreadConfigSnapshot { pub model: String, @@ -64,6 +66,10 @@ impl CodexThread { self.rollout_path.clone() } + pub fn state_db(&self) -> Option { + self.codex.state_db() + } + pub async fn config_snapshot(&self) -> ThreadConfigSnapshot { self.codex.thread_config_snapshot().await } diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index c365d2cfda..b233618eea 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -10,7 +10,6 @@ use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::features::Feature; use crate::protocol::CompactedItem; -use crate::protocol::ContextCompactedEvent; use crate::protocol::EventMsg; use crate::protocol::TurnContextItem; use crate::protocol::TurnStartedEvent; @@ -20,6 +19,7 @@ use crate::truncate::TruncationPolicy; use crate::truncate::approx_token_count; use crate::truncate::truncate_text; use crate::util::backoff; +use codex_protocol::items::ContextCompactionItem; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; @@ -71,6 +71,9 @@ async fn run_compact_task_inner( turn_context: Arc, input: Vec, ) { + let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); + sess.emit_turn_item_started(&turn_context, &compaction_item) + .await; let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input); let mut history = sess.clone_history().await; @@ -193,9 +196,8 @@ async fn run_compact_task_inner( }); sess.persist_rollout_items(&[rollout_item]).await; - let event = EventMsg::ContextCompacted(ContextCompactedEvent {}); - sess.send_event(&turn_context, event).await; - + sess.emit_turn_item_completed(&turn_context, compaction_item) + .await; let warning = EventMsg::Warning(WarningEvent { message: "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.".to_string(), }); diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index aaa7fc68a7..35c157acfc 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -5,10 +5,11 @@ use crate::codex::Session; use crate::codex::TurnContext; use crate::error::Result as CodexResult; use crate::protocol::CompactedItem; -use crate::protocol::ContextCompactedEvent; use crate::protocol::EventMsg; use crate::protocol::RolloutItem; use crate::protocol::TurnStartedEvent; +use codex_protocol::items::ContextCompactionItem; +use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; pub(crate) async fn run_inline_remote_auto_compact_task( @@ -40,6 +41,9 @@ async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, ) -> CodexResult<()> { + let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); + sess.emit_turn_item_started(turn_context, &compaction_item) + .await; let history = sess.clone_history().await; // Required to keep `/undo` available after compaction @@ -77,8 +81,7 @@ async fn run_remote_compact_task_inner_impl( sess.persist_rollout_items(&[RolloutItem::Compacted(compacted_item)]) .await; - let event = EventMsg::ContextCompacted(ContextCompactedEvent {}); - sess.send_event(turn_context, event).await; - + sess.emit_turn_item_completed(turn_context, compaction_item) + .await; Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8744b756bf..895e54c040 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -7,6 +7,7 @@ use crate::config::types::McpServerConfig; use crate::config::types::McpServerDisabledReason; use crate::config::types::McpServerTransportConfig; use crate::config::types::Notice; +use crate::config::types::NotificationMethod; use crate::config::types::Notifications; use crate::config::types::OtelConfig; use crate::config::types::OtelConfigToml; @@ -38,6 +39,7 @@ use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; +use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; use codex_protocol::config_types::AltScreenMode; @@ -49,6 +51,7 @@ use codex_protocol::config_types::SandboxMode; 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::openai_models::ReasoningEffort; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; @@ -190,10 +193,13 @@ pub struct Config { /// If unset the feature is disabled. pub notify: Option>, - /// TUI notifications preference. When set, the TUI will send OSC 9 notifications on approvals - /// and turn completions when not focused. + /// TUI notifications preference. When set, the TUI will send terminal notifications on + /// approvals and turn completions when not focused. pub tui_notifications: Notifications, + /// Notification method for terminal notifications (osc9 or bel). + pub tui_notification_method: NotificationMethod, + /// Enable ASCII animations and shimmer effects in the TUI. pub animations: bool, @@ -304,8 +310,8 @@ pub struct Config { /// model info's default preference. pub include_apply_patch_tool: bool, - /// Explicit or feature-derived web search mode. Defaults to cached. - pub web_search_mode: WebSearchMode, + /// Explicit or feature-derived web search mode. + pub web_search_mode: Option, /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, @@ -1056,6 +1062,7 @@ impl ConfigToml { &self, sandbox_mode_override: Option, profile_sandbox_mode: Option, + windows_sandbox_level: WindowsSandboxLevel, resolved_cwd: &Path, ) -> SandboxPolicyResolution { let resolved_sandbox_mode = sandbox_mode_override @@ -1094,7 +1101,7 @@ impl ConfigToml { if cfg!(target_os = "windows") && matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) // If the experimental Windows sandbox is enabled, do not force a downgrade. - && crate::safety::get_platform_sandbox().is_none() + && windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled { sandbox_policy = SandboxPolicy::new_read_only_policy(); forced_auto_mode_downgraded_on_windows = true; @@ -1205,17 +1212,31 @@ fn resolve_web_search_mode( config_toml: &ConfigToml, config_profile: &ConfigProfile, features: &Features, -) -> WebSearchMode { +) -> Option { if let Some(mode) = config_profile.web_search.or(config_toml.web_search) { - return mode; + return Some(mode); } if features.enabled(Feature::WebSearchCached) { - return WebSearchMode::Cached; + return Some(WebSearchMode::Cached); } if features.enabled(Feature::WebSearchRequest) { - return WebSearchMode::Live; + return Some(WebSearchMode::Live); + } + None +} + +pub(crate) fn resolve_web_search_mode_for_turn( + explicit_mode: Option, + sandbox_policy: &SandboxPolicy, +) -> WebSearchMode { + if let Some(mode) = explicit_mode { + return mode; + } + if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) { + WebSearchMode::Live + } else { + WebSearchMode::Cached } - WebSearchMode::Cached } impl Config { @@ -1284,17 +1305,6 @@ impl Config { }; let features = Features::from_config(&cfg, &config_profile, feature_overrides); - let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features); - #[cfg(target_os = "windows")] - { - // Base flag controls sandbox on/off; elevated only applies when base is enabled. - let sandbox_enabled = features.enabled(Feature::WindowsSandbox); - crate::safety::set_windows_sandbox_enabled(sandbox_enabled); - let elevated_enabled = - sandbox_enabled && features.enabled(Feature::WindowsSandboxElevated); - crate::safety::set_windows_elevated_sandbox_enabled(elevated_enabled); - } - let resolved_cwd = { use std::env; @@ -1321,10 +1331,16 @@ impl Config { .get_active_project(&resolved_cwd) .unwrap_or(ProjectConfig { trust_level: None }); + let windows_sandbox_level = WindowsSandboxLevel::from_features(&features); let SandboxPolicyResolution { policy: mut sandbox_policy, forced_auto_mode_downgraded_on_windows, - } = cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd); + } = cfg.derive_sandbox_policy( + sandbox_mode, + config_profile.sandbox_mode, + windows_sandbox_level, + &resolved_cwd, + ); if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { for path in additional_writable_roots { if !writable_roots.iter().any(|existing| existing == &path) { @@ -1344,6 +1360,7 @@ impl Config { AskForApproval::default() } }); + let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features); // TODO(dylan): We should be able to leverage ConfigLayerStack so that // we can reliably check this at every config level. let did_user_set_custom_approval_policy_or_sandbox_mode = approval_policy_override @@ -1594,6 +1611,11 @@ impl Config { .as_ref() .map(|t| t.notifications.clone()) .unwrap_or_default(), + tui_notification_method: cfg + .tui + .as_ref() + .map(|t| t.notification_method) + .unwrap_or_default(), animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true), show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true), experimental_mode: cfg.tui.as_ref().and_then(|t| t.experimental_mode), @@ -1666,20 +1688,19 @@ impl Config { } } - pub fn set_windows_sandbox_globally(&mut self, value: bool) { - crate::safety::set_windows_sandbox_enabled(value); + pub fn set_windows_sandbox_enabled(&mut self, value: bool) { if value { self.features.enable(Feature::WindowsSandbox); + self.forced_auto_mode_downgraded_on_windows = false; } else { self.features.disable(Feature::WindowsSandbox); } - self.forced_auto_mode_downgraded_on_windows = !value; } - pub fn set_windows_elevated_sandbox_globally(&mut self, value: bool) { - crate::safety::set_windows_elevated_sandbox_enabled(value); + pub fn set_windows_elevated_sandbox_enabled(&mut self, value: bool) { if value { self.features.enable(Feature::WindowsSandboxElevated); + self.forced_auto_mode_downgraded_on_windows = false; } else { self.features.disable(Feature::WindowsSandboxElevated); } @@ -1753,6 +1774,7 @@ mod tests { use crate::config::types::FeedbackConfigToml; use crate::config::types::HistoryPersistence; use crate::config::types::McpServerTransportConfig; + use crate::config::types::NotificationMethod; use crate::config::types::Notifications; use crate::config_loader::RequirementSource; use crate::features::Feature; @@ -1849,6 +1871,7 @@ persistence = "none" tui, Tui { notifications: Notifications::Enabled(true), + notification_method: NotificationMethod::Auto, animations: true, show_tooltips: true, experimental_mode: None, @@ -1871,6 +1894,7 @@ network_access = false # This should be ignored. let resolution = sandbox_full_access_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); assert_eq!( @@ -1894,6 +1918,7 @@ network_access = true # This should be ignored. let resolution = sandbox_read_only_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); assert_eq!( @@ -1925,6 +1950,7 @@ exclude_slash_tmp = true let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); if cfg!(target_os = "windows") { @@ -1973,6 +1999,7 @@ trust_level = "trusted" let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); if cfg!(target_os = "windows") { @@ -2264,15 +2291,12 @@ trust_level = "trusted" } #[test] - fn web_search_mode_defaults_to_cached_if_unset() { + fn web_search_mode_defaults_to_none_if_unset() { let cfg = ConfigToml::default(); let profile = ConfigProfile::default(); let features = Features::with_defaults(); - assert_eq!( - resolve_web_search_mode(&cfg, &profile, &features), - WebSearchMode::Cached - ); + assert_eq!(resolve_web_search_mode(&cfg, &profile, &features), None); } #[test] @@ -2287,7 +2311,7 @@ trust_level = "trusted" assert_eq!( resolve_web_search_mode(&cfg, &profile, &features), - WebSearchMode::Live + Some(WebSearchMode::Live) ); } @@ -2303,10 +2327,34 @@ trust_level = "trusted" assert_eq!( resolve_web_search_mode(&cfg, &profile, &features), - WebSearchMode::Disabled + Some(WebSearchMode::Disabled) ); } + #[test] + fn web_search_mode_for_turn_defaults_to_cached_when_unset() { + let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::ReadOnly); + + assert_eq!(mode, WebSearchMode::Cached); + } + + #[test] + fn web_search_mode_for_turn_defaults_to_live_for_danger_full_access() { + let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::DangerFullAccess); + + assert_eq!(mode, WebSearchMode::Live); + } + + #[test] + fn web_search_mode_for_turn_prefers_explicit_value() { + let mode = resolve_web_search_mode_for_turn( + Some(WebSearchMode::Cached), + &SandboxPolicy::DangerFullAccess, + ); + + assert_eq!(mode, WebSearchMode::Cached); + } + #[test] fn profile_legacy_toggles_override_base() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -3740,7 +3788,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -3752,6 +3800,7 @@ model_verbosity = "high" check_for_update_on_startup: true, disable_paste_burst: false, tui_notifications: Default::default(), + tui_notification_method: Default::default(), animations: true, show_tooltips: true, experimental_mode: None, @@ -3823,7 +3872,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -3835,6 +3884,7 @@ model_verbosity = "high" check_for_update_on_startup: true, disable_paste_burst: false, tui_notifications: Default::default(), + tui_notification_method: Default::default(), animations: true, show_tooltips: true, experimental_mode: None, @@ -3921,7 +3971,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -3933,6 +3983,7 @@ model_verbosity = "high" check_for_update_on_startup: true, disable_paste_burst: false, tui_notifications: Default::default(), + tui_notification_method: Default::default(), animations: true, show_tooltips: true, experimental_mode: None, @@ -4005,7 +4056,7 @@ model_verbosity = "high" forced_chatgpt_workspace_id: None, forced_login_method: None, include_apply_patch_tool: false, - web_search_mode: WebSearchMode::Cached, + web_search_mode: None, use_experimental_unified_exec_tool: false, ghost_snapshot: GhostSnapshotConfig::default(), features: Features::with_defaults(), @@ -4017,6 +4068,7 @@ model_verbosity = "high" check_for_update_on_startup: true, disable_paste_burst: false, tui_notifications: Default::default(), + tui_notification_method: Default::default(), animations: true, show_tooltips: true, experimental_mode: None, @@ -4190,7 +4242,12 @@ trust_level = "untrusted" let cfg = toml::from_str::(config_with_untrusted) .expect("TOML deserialization should succeed"); - let resolution = cfg.derive_sandbox_policy(None, None, &PathBuf::from("/tmp/test")); + let resolution = cfg.derive_sandbox_policy( + None, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + ); // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) if cfg!(target_os = "windows") { @@ -4369,13 +4426,17 @@ mcp_oauth_callback_port = 5678 #[cfg(test)] mod notifications_tests { + use crate::config::types::NotificationMethod; use crate::config::types::Notifications; use assert_matches::assert_matches; use serde::Deserialize; #[derive(Deserialize, Debug, PartialEq)] struct TuiTomlTest { + #[serde(default)] notifications: Notifications, + #[serde(default)] + notification_method: NotificationMethod, } #[derive(Deserialize, Debug, PartialEq)] @@ -4406,4 +4467,15 @@ mod notifications_tests { Notifications::Custom(ref v) if v == &vec!["foo".to_string()] ); } + + #[test] + fn test_tui_notification_method() { + let toml = r#" + [tui] + notification_method = "bel" + "#; + let parsed: RootTomlTest = + toml::from_str(toml).expect("deserialize notification_method=\"bel\""); + assert_eq!(parsed.tui.notification_method, NotificationMethod::Bel); + } } diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 96379dfa03..e949d869a6 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -428,6 +428,25 @@ impl Default for Notifications { } } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, Default)] +#[serde(rename_all = "lowercase")] +pub enum NotificationMethod { + #[default] + Auto, + Osc9, + Bel, +} + +impl fmt::Display for NotificationMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NotificationMethod::Auto => write!(f, "auto"), + NotificationMethod::Osc9 => write!(f, "osc9"), + NotificationMethod::Bel => write!(f, "bel"), + } + } +} + /// Collection of settings that are specific to the TUI. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] #[schemars(deny_unknown_fields)] @@ -437,6 +456,11 @@ pub struct Tui { #[serde(default)] pub notifications: Notifications, + /// Notification method to use for unfocused terminal notifications. + /// Defaults to `auto`. + #[serde(default)] + pub notification_method: NotificationMethod, + /// Enable animations (welcome screen, shimmer effects, spinners). /// Defaults to `true`. #[serde(default = "default_true")] @@ -472,7 +496,6 @@ const fn default_true() -> bool { /// (primarily the Codex IDE extension). NOTE: these are different from /// notifications - notices are warnings, NUX screens, acknowledgements, etc. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] -#[schemars(deny_unknown_fields)] pub struct Notice { /// Tracks whether the user has acknowledged the full access warning prompt. pub hide_full_access_warning: Option, diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index ae56c384e4..5db10f2808 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -6,6 +6,8 @@ mod layer_io; mod macos; mod merge; mod overrides; +#[cfg(test)] +mod requirements_exec_policy; mod state; #[cfg(test)] diff --git a/codex-rs/core/src/config_loader/requirements_exec_policy.rs b/codex-rs/core/src/config_loader/requirements_exec_policy.rs new file mode 100644 index 0000000000..cbc1d7531e --- /dev/null +++ b/codex-rs/core/src/config_loader/requirements_exec_policy.rs @@ -0,0 +1,188 @@ +use codex_execpolicy::Decision; +use codex_execpolicy::Policy; +use codex_execpolicy::rule::PatternToken; +use codex_execpolicy::rule::PrefixPattern; +use codex_execpolicy::rule::PrefixRule; +use codex_execpolicy::rule::RuleRef; +use multimap::MultiMap; +use serde::Deserialize; +use std::sync::Arc; +use thiserror::Error; + +/// TOML types for expressing exec policy requirements. +/// +/// These types are kept separate from `ConfigRequirementsToml` and are +/// converted into `codex-execpolicy` rules. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct RequirementsExecPolicyTomlRoot { + pub exec_policy: RequirementsExecPolicyToml, +} + +/// TOML representation of `[exec_policy]` within `requirements.toml`. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct RequirementsExecPolicyToml { + pub prefix_rules: Vec, +} + +/// A TOML representation of the `prefix_rule(...)` Starlark builtin. +/// +/// This mirrors the builtin defined in `execpolicy/src/parser.rs`. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct RequirementsExecPolicyPrefixRuleToml { + pub pattern: Vec, + pub decision: Option, + pub justification: Option, +} + +/// TOML-friendly representation of a pattern token. +/// +/// Starlark supports either a string token or a list of alternative tokens at +/// each position, but TOML arrays cannot mix strings and arrays. Using an +/// array of tables sidesteps that restriction. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct RequirementsExecPolicyPatternTokenToml { + pub token: Option, + pub any_of: Option>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum RequirementsExecPolicyDecisionToml { + Allow, + Prompt, + Forbidden, +} + +impl RequirementsExecPolicyDecisionToml { + fn as_decision(self) -> Decision { + match self { + Self::Allow => Decision::Allow, + Self::Prompt => Decision::Prompt, + Self::Forbidden => Decision::Forbidden, + } + } +} + +#[derive(Debug, Error)] +pub enum RequirementsExecPolicyParseError { + #[error("exec policy prefix_rules cannot be empty")] + EmptyPrefixRules, + + #[error("exec policy prefix_rule at index {rule_index} has an empty pattern")] + EmptyPattern { rule_index: usize }, + + #[error( + "exec policy prefix_rule at index {rule_index} has an invalid pattern token at index {token_index}: {reason}" + )] + InvalidPatternToken { + rule_index: usize, + token_index: usize, + reason: String, + }, + + #[error("exec policy prefix_rule at index {rule_index} has an empty justification")] + EmptyJustification { rule_index: usize }, +} + +impl RequirementsExecPolicyToml { + /// Convert requirements TOML exec policy rules into the internal `.rules` + /// representation used by `codex-execpolicy`. + pub fn to_policy(&self) -> Result { + if self.prefix_rules.is_empty() { + return Err(RequirementsExecPolicyParseError::EmptyPrefixRules); + } + + let mut rules_by_program: MultiMap = MultiMap::new(); + + for (rule_index, rule) in self.prefix_rules.iter().enumerate() { + if let Some(justification) = &rule.justification + && justification.trim().is_empty() + { + return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index }); + } + + if rule.pattern.is_empty() { + return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index }); + } + + let pattern_tokens = rule + .pattern + .iter() + .enumerate() + .map(|(token_index, token)| parse_pattern_token(token, rule_index, token_index)) + .collect::, _>>()?; + + let decision = rule + .decision + .map(RequirementsExecPolicyDecisionToml::as_decision) + .unwrap_or(Decision::Allow); + let justification = rule.justification.clone(); + + let (first_token, remaining_tokens) = pattern_tokens + .split_first() + .ok_or(RequirementsExecPolicyParseError::EmptyPattern { rule_index })?; + + let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into(); + + for head in first_token.alternatives() { + let rule: RuleRef = Arc::new(PrefixRule { + pattern: PrefixPattern { + first: Arc::from(head.as_str()), + rest: rest.clone(), + }, + decision, + justification: justification.clone(), + }); + rules_by_program.insert(head.clone(), rule); + } + } + + Ok(Policy::new(rules_by_program)) + } +} + +fn parse_pattern_token( + token: &RequirementsExecPolicyPatternTokenToml, + rule_index: usize, + token_index: usize, +) -> Result { + match (&token.token, &token.any_of) { + (Some(single), None) => { + if single.trim().is_empty() { + return Err(RequirementsExecPolicyParseError::InvalidPatternToken { + rule_index, + token_index, + reason: "token cannot be empty".to_string(), + }); + } + Ok(PatternToken::Single(single.clone())) + } + (None, Some(alternatives)) => { + if alternatives.is_empty() { + return Err(RequirementsExecPolicyParseError::InvalidPatternToken { + rule_index, + token_index, + reason: "any_of cannot be empty".to_string(), + }); + } + if alternatives.iter().any(|alt| alt.trim().is_empty()) { + return Err(RequirementsExecPolicyParseError::InvalidPatternToken { + rule_index, + token_index, + reason: "any_of cannot include empty tokens".to_string(), + }); + } + Ok(PatternToken::Alts(alternatives.clone())) + } + (Some(_), Some(_)) => Err(RequirementsExecPolicyParseError::InvalidPatternToken { + rule_index, + token_index, + reason: "set either token or any_of, not both".to_string(), + }), + (None, None) => Err(RequirementsExecPolicyParseError::InvalidPatternToken { + rule_index, + token_index, + reason: "set either token or any_of".to_string(), + }), + } +} diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 03b0706c84..4b512c150f 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -911,3 +911,165 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<() Ok(()) } + +mod requirements_exec_policy_tests { + use super::super::requirements_exec_policy::RequirementsExecPolicyDecisionToml; + use super::super::requirements_exec_policy::RequirementsExecPolicyPatternTokenToml; + use super::super::requirements_exec_policy::RequirementsExecPolicyPrefixRuleToml; + use super::super::requirements_exec_policy::RequirementsExecPolicyToml; + use super::super::requirements_exec_policy::RequirementsExecPolicyTomlRoot; + use codex_execpolicy::Decision; + use codex_execpolicy::Evaluation; + use codex_execpolicy::RuleMatch; + use pretty_assertions::assert_eq; + use toml::from_str; + + fn tokens(cmd: &[&str]) -> Vec { + cmd.iter().map(std::string::ToString::to_string).collect() + } + + fn allow_all(_: &[String]) -> Decision { + Decision::Allow + } + + #[test] + fn parses_single_prefix_rule_from_raw_toml() -> anyhow::Result<()> { + let toml_str = r#" +[exec_policy] +prefix_rules = [ + { pattern = [{ token = "rm" }], decision = "forbidden" }, +] +"#; + + let parsed: RequirementsExecPolicyTomlRoot = from_str(toml_str)?; + + assert_eq!( + parsed, + RequirementsExecPolicyTomlRoot { + exec_policy: RequirementsExecPolicyToml { + prefix_rules: vec![RequirementsExecPolicyPrefixRuleToml { + pattern: vec![RequirementsExecPolicyPatternTokenToml { + token: Some("rm".to_string()), + any_of: None, + }], + decision: Some(RequirementsExecPolicyDecisionToml::Forbidden), + justification: None, + }], + }, + } + ); + + Ok(()) + } + + #[test] + fn parses_multiple_prefix_rules_from_raw_toml() -> anyhow::Result<()> { + let toml_str = r#" +[exec_policy] +prefix_rules = [ + { pattern = [{ token = "rm" }], decision = "forbidden" }, + { pattern = [{ token = "git" }, { any_of = ["push", "commit"] }], decision = "prompt", justification = "review changes before push or commit" }, +] +"#; + + let parsed: RequirementsExecPolicyTomlRoot = from_str(toml_str)?; + + assert_eq!( + parsed, + RequirementsExecPolicyTomlRoot { + exec_policy: RequirementsExecPolicyToml { + prefix_rules: vec![ + RequirementsExecPolicyPrefixRuleToml { + pattern: vec![RequirementsExecPolicyPatternTokenToml { + token: Some("rm".to_string()), + any_of: None, + }], + decision: Some(RequirementsExecPolicyDecisionToml::Forbidden), + justification: None, + }, + RequirementsExecPolicyPrefixRuleToml { + pattern: vec![ + RequirementsExecPolicyPatternTokenToml { + token: Some("git".to_string()), + any_of: None, + }, + RequirementsExecPolicyPatternTokenToml { + token: None, + any_of: Some(vec!["push".to_string(), "commit".to_string()]), + }, + ], + decision: Some(RequirementsExecPolicyDecisionToml::Prompt), + justification: Some("review changes before push or commit".to_string()), + }, + ], + }, + } + ); + + Ok(()) + } + + #[test] + fn converts_rules_toml_into_internal_policy_representation() -> anyhow::Result<()> { + let toml_str = r#" +[exec_policy] +prefix_rules = [ + { pattern = [{ token = "rm" }], decision = "forbidden" }, +] +"#; + + let parsed: RequirementsExecPolicyTomlRoot = from_str(toml_str)?; + let policy = parsed.exec_policy.to_policy()?; + + assert_eq!( + policy.check(&tokens(&["rm", "-rf", "/tmp"]), &allow_all), + Evaluation { + decision: Decision::Forbidden, + matched_rules: vec![RuleMatch::PrefixRuleMatch { + matched_prefix: tokens(&["rm"]), + decision: Decision::Forbidden, + justification: None, + }], + } + ); + + Ok(()) + } + + #[test] + fn head_any_of_expands_into_multiple_program_rules() -> anyhow::Result<()> { + let toml_str = r#" +[exec_policy] +prefix_rules = [ + { pattern = [{ any_of = ["git", "hg"] }, { token = "status" }], decision = "prompt" }, +] +"#; + let parsed: RequirementsExecPolicyTomlRoot = from_str(toml_str)?; + let policy = parsed.exec_policy.to_policy()?; + + assert_eq!( + policy.check(&tokens(&["git", "status"]), &allow_all), + Evaluation { + decision: Decision::Prompt, + matched_rules: vec![RuleMatch::PrefixRuleMatch { + matched_prefix: tokens(&["git", "status"]), + decision: Decision::Prompt, + justification: None, + }], + } + ); + assert_eq!( + policy.check(&tokens(&["hg", "status"]), &allow_all), + Evaluation { + decision: Decision::Prompt, + matched_rules: vec![RuleMatch::PrefixRuleMatch { + matched_prefix: tokens(&["hg", "status"]), + decision: Decision::Prompt, + justification: None, + }], + } + ); + + Ok(()) + } +} diff --git a/codex-rs/core/src/default_client.rs b/codex-rs/core/src/default_client.rs index 4ded10a3d9..67a5daf1b1 100644 --- a/codex-rs/core/src/default_client.rs +++ b/codex-rs/core/src/default_client.rs @@ -95,6 +95,12 @@ pub fn originator() -> Originator { get_originator_value(None) } +pub fn is_first_party_originator(originator_value: &str) -> bool { + originator_value == DEFAULT_ORIGINATOR + || originator_value == "codex_vscode" + || originator_value.starts_with("Codex ") +} + pub fn get_codex_user_agent() -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); @@ -185,6 +191,7 @@ fn is_sandboxed() -> bool { mod tests { use super::*; use core_test_support::skip_if_no_network; + use pretty_assertions::assert_eq; #[test] fn test_get_codex_user_agent() { @@ -194,6 +201,15 @@ mod tests { assert!(user_agent.starts_with(&prefix)); } + #[test] + fn is_first_party_originator_matches_known_values() { + assert_eq!(is_first_party_originator(DEFAULT_ORIGINATOR), true); + assert_eq!(is_first_party_originator("codex_vscode"), true); + assert_eq!(is_first_party_originator("Codex Something Else"), true); + assert_eq!(is_first_party_originator("codex_cli"), false); + assert_eq!(is_first_party_originator("Other"), false); + } + #[tokio::test] async fn test_create_client_sets_default_headers() { skip_if_no_network!(); diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 275f2fb856..37f6c2de96 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -64,6 +64,7 @@ pub struct ExecParams { pub expiration: ExecExpiration, pub env: HashMap, pub sandbox_permissions: SandboxPermissions, + pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub justification: Option, pub arg0: Option, } @@ -141,11 +142,15 @@ pub async fn process_exec_tool_call( codex_linux_sandbox_exe: &Option, stdout_stream: Option, ) -> Result { + let windows_sandbox_level = params.windows_sandbox_level; let sandbox_type = match &sandbox_policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { SandboxType::None } - _ => get_platform_sandbox().unwrap_or(SandboxType::None), + _ => get_platform_sandbox( + windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None), }; tracing::debug!("Sandbox type: {sandbox_type:?}"); @@ -155,6 +160,7 @@ pub async fn process_exec_tool_call( expiration, env, sandbox_permissions, + windows_sandbox_level, justification, arg0: _, } = params; @@ -184,6 +190,7 @@ pub async fn process_exec_tool_call( sandbox_type, sandbox_cwd, codex_linux_sandbox_exe.as_ref(), + windows_sandbox_level, ) .map_err(CodexErr::from)?; @@ -202,6 +209,7 @@ pub(crate) async fn execute_exec_env( env, expiration, sandbox, + windows_sandbox_level, sandbox_permissions, justification, arg0, @@ -213,6 +221,7 @@ pub(crate) async fn execute_exec_env( expiration, env, sandbox_permissions, + windows_sandbox_level, justification, arg0, }; @@ -229,7 +238,7 @@ async fn exec_windows_sandbox( sandbox_policy: &SandboxPolicy, ) -> Result { use crate::config::find_codex_home; - use crate::safety::is_windows_elevated_sandbox_enabled; + use codex_protocol::config_types::WindowsSandboxLevel; use codex_windows_sandbox::run_windows_sandbox_capture; use codex_windows_sandbox::run_windows_sandbox_capture_elevated; @@ -238,6 +247,7 @@ async fn exec_windows_sandbox( cwd, env, expiration, + windows_sandbox_level, .. } = params; // TODO(iceweasel-oai): run_windows_sandbox_capture should support all @@ -255,7 +265,7 @@ async fn exec_windows_sandbox( "windows sandbox: failed to resolve codex_home: {err}" ))) })?; - let use_elevated = is_windows_elevated_sandbox_enabled(); + let use_elevated = matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated); let spawn_res = tokio::task::spawn_blocking(move || { if use_elevated { run_windows_sandbox_capture_elevated( @@ -312,20 +322,7 @@ async fn exec_windows_sandbox( text: stderr_text, truncated_after_lines: None, }; - // Best-effort aggregate: stdout then stderr (capped). - let mut aggregated = Vec::with_capacity( - stdout - .text - .len() - .saturating_add(stderr.text.len()) - .min(EXEC_OUTPUT_MAX_BYTES), - ); - append_capped(&mut aggregated, &stdout.text, EXEC_OUTPUT_MAX_BYTES); - append_capped(&mut aggregated, &stderr.text, EXEC_OUTPUT_MAX_BYTES); - let aggregated_output = StreamOutput { - text: aggregated, - truncated_after_lines: None, - }; + let aggregated_output = aggregate_output(&stdout, &stderr); Ok(RawExecToolCallOutput { exit_status, @@ -519,6 +516,39 @@ fn append_capped(dst: &mut Vec, src: &[u8], max_bytes: usize) { dst.extend_from_slice(&src[..take]); } +fn aggregate_output( + stdout: &StreamOutput>, + stderr: &StreamOutput>, +) -> StreamOutput> { + let total_len = stdout.text.len().saturating_add(stderr.text.len()); + let max_bytes = EXEC_OUTPUT_MAX_BYTES; + let mut aggregated = Vec::with_capacity(total_len.min(max_bytes)); + + if total_len <= max_bytes { + aggregated.extend_from_slice(&stdout.text); + aggregated.extend_from_slice(&stderr.text); + return StreamOutput { + text: aggregated, + truncated_after_lines: None, + }; + } + + // Under contention, reserve 1/3 for stdout and 2/3 for stderr; rebalance unused stderr to stdout. + let want_stdout = stdout.text.len().min(max_bytes / 3); + let want_stderr = stderr.text.len(); + let stderr_take = want_stderr.min(max_bytes.saturating_sub(want_stdout)); + let remaining = max_bytes.saturating_sub(want_stdout + stderr_take); + let stdout_take = want_stdout + remaining.min(stdout.text.len().saturating_sub(want_stdout)); + + aggregated.extend_from_slice(&stdout.text[..stdout_take]); + aggregated.extend_from_slice(&stderr.text[..stderr_take]); + + StreamOutput { + text: aggregated, + truncated_after_lines: None, + } +} + #[derive(Clone, Debug)] pub struct ExecToolCallOutput { pub exit_code: i32, @@ -564,6 +594,7 @@ async fn exec( env, arg0, expiration, + windows_sandbox_level: _, .. } = params; @@ -683,20 +714,7 @@ async fn consume_truncated_output( Duration::from_millis(IO_DRAIN_TIMEOUT_MS), ) .await?; - // Best-effort aggregate: stdout then stderr (capped). - let mut aggregated = Vec::with_capacity( - stdout - .text - .len() - .saturating_add(stderr.text.len()) - .min(EXEC_OUTPUT_MAX_BYTES), - ); - append_capped(&mut aggregated, &stdout.text, EXEC_OUTPUT_MAX_BYTES); - append_capped(&mut aggregated, &stderr.text, EXEC_OUTPUT_MAX_BYTES * 2); - let aggregated_output = StreamOutput { - text: aggregated, - truncated_after_lines: None, - }; + let aggregated_output = aggregate_output(&stdout, &stderr); Ok(RawExecToolCallOutput { exit_status, @@ -771,6 +789,7 @@ fn synthetic_exit_status(code: i32) -> ExitStatus { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; use std::time::Duration; use tokio::io::AsyncWriteExt; @@ -846,6 +865,85 @@ mod tests { assert_eq!(out.text.len(), EXEC_OUTPUT_MAX_BYTES); } + #[test] + fn aggregate_output_prefers_stderr_on_contention() { + let stdout = StreamOutput { + text: vec![b'a'; EXEC_OUTPUT_MAX_BYTES], + truncated_after_lines: None, + }; + let stderr = StreamOutput { + text: vec![b'b'; EXEC_OUTPUT_MAX_BYTES], + truncated_after_lines: None, + }; + + let aggregated = aggregate_output(&stdout, &stderr); + let stdout_cap = EXEC_OUTPUT_MAX_BYTES / 3; + let stderr_cap = EXEC_OUTPUT_MAX_BYTES.saturating_sub(stdout_cap); + + assert_eq!(aggregated.text.len(), EXEC_OUTPUT_MAX_BYTES); + assert_eq!(aggregated.text[..stdout_cap], vec![b'a'; stdout_cap]); + assert_eq!(aggregated.text[stdout_cap..], vec![b'b'; stderr_cap]); + } + + #[test] + fn aggregate_output_fills_remaining_capacity_with_stderr() { + let stdout_len = EXEC_OUTPUT_MAX_BYTES / 10; + let stdout = StreamOutput { + text: vec![b'a'; stdout_len], + truncated_after_lines: None, + }; + let stderr = StreamOutput { + text: vec![b'b'; EXEC_OUTPUT_MAX_BYTES], + truncated_after_lines: None, + }; + + let aggregated = aggregate_output(&stdout, &stderr); + let stderr_cap = EXEC_OUTPUT_MAX_BYTES.saturating_sub(stdout_len); + + assert_eq!(aggregated.text.len(), EXEC_OUTPUT_MAX_BYTES); + assert_eq!(aggregated.text[..stdout_len], vec![b'a'; stdout_len]); + assert_eq!(aggregated.text[stdout_len..], vec![b'b'; stderr_cap]); + } + + #[test] + fn aggregate_output_rebalances_when_stderr_is_small() { + let stdout = StreamOutput { + text: vec![b'a'; EXEC_OUTPUT_MAX_BYTES], + truncated_after_lines: None, + }; + let stderr = StreamOutput { + text: vec![b'b'; 1], + truncated_after_lines: None, + }; + + let aggregated = aggregate_output(&stdout, &stderr); + let stdout_len = EXEC_OUTPUT_MAX_BYTES.saturating_sub(1); + + assert_eq!(aggregated.text.len(), EXEC_OUTPUT_MAX_BYTES); + assert_eq!(aggregated.text[..stdout_len], vec![b'a'; stdout_len]); + assert_eq!(aggregated.text[stdout_len..], vec![b'b'; 1]); + } + + #[test] + fn aggregate_output_keeps_stdout_then_stderr_when_under_cap() { + let stdout = StreamOutput { + text: vec![b'a'; 4], + truncated_after_lines: None, + }; + let stderr = StreamOutput { + text: vec![b'b'; 3], + truncated_after_lines: None, + }; + + let aggregated = aggregate_output(&stdout, &stderr); + let mut expected = Vec::new(); + expected.extend_from_slice(&stdout.text); + expected.extend_from_slice(&stderr.text); + + assert_eq!(aggregated.text, expected); + assert_eq!(aggregated.truncated_after_lines, None); + } + #[cfg(unix)] #[test] fn sandbox_detection_flags_sigsys_exit_code() { @@ -878,6 +976,7 @@ mod tests { expiration: 500.into(), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; @@ -923,6 +1022,7 @@ mod tests { expiration: ExecExpiration::Cancellation(cancel_token), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 61c0070239..07a9ebceba 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -87,6 +87,15 @@ pub(crate) struct ExecPolicyManager { policy: ArcSwap, } +pub(crate) struct ExecApprovalRequest<'a> { + pub(crate) features: &'a Features, + pub(crate) command: &'a [String], + pub(crate) approval_policy: AskForApproval, + pub(crate) sandbox_policy: &'a SandboxPolicy, + pub(crate) sandbox_permissions: SandboxPermissions, + pub(crate) prefix_rule: Option>, +} + impl ExecPolicyManager { pub(crate) fn new(policy: Arc) -> Self { Self { @@ -112,12 +121,16 @@ impl ExecPolicyManager { pub(crate) async fn create_exec_approval_requirement_for_command( &self, - features: &Features, - command: &[String], - approval_policy: AskForApproval, - sandbox_policy: &SandboxPolicy, - sandbox_permissions: SandboxPermissions, + req: ExecApprovalRequest<'_>, ) -> ExecApprovalRequirement { + let ExecApprovalRequest { + features, + command, + approval_policy, + sandbox_policy, + sandbox_permissions, + prefix_rule, + } = req; let exec_policy = self.current(); let commands = parse_shell_lc_plain_commands(command).unwrap_or_else(|| vec![command.to_vec()]); @@ -131,6 +144,12 @@ impl ExecPolicyManager { }; let evaluation = exec_policy.check_multiple(commands.iter(), &exec_policy_fallback); + let requested_amendment = derive_requested_execpolicy_amendment( + features, + prefix_rule.as_ref(), + &evaluation.matched_rules, + ); + match evaluation.decision { Decision::Forbidden => ExecApprovalRequirement::Forbidden { reason: derive_forbidden_reason(command, &evaluation), @@ -144,9 +163,11 @@ impl ExecPolicyManager { ExecApprovalRequirement::NeedsApproval { reason: derive_prompt_reason(command, &evaluation), proposed_execpolicy_amendment: if features.enabled(Feature::ExecPolicy) { - try_derive_execpolicy_amendment_for_prompt_rules( - &evaluation.matched_rules, - ) + requested_amendment.or_else(|| { + try_derive_execpolicy_amendment_for_prompt_rules( + &evaluation.matched_rules, + ) + }) } else { None }, @@ -382,6 +403,30 @@ fn try_derive_execpolicy_amendment_for_allow_rules( }) } +fn derive_requested_execpolicy_amendment( + features: &Features, + prefix_rule: Option<&Vec>, + matched_rules: &[RuleMatch], +) -> Option { + if !features.enabled(Feature::ExecPolicy) { + return None; + } + + let prefix_rule = prefix_rule?; + if prefix_rule.is_empty() { + return None; + } + + if matched_rules + .iter() + .any(|rule_match| is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt) + { + return None; + } + + Some(ExecPolicyAmendment::new(prefix_rule.clone())) +} + /// Only return a reason when a policy rule drove the prompt decision. fn derive_prompt_reason(command_args: &[String], evaluation: &Evaluation) -> Option { let command = render_shlex_command(command_args); @@ -756,13 +801,14 @@ prefix_rule(pattern=["rm"], decision="forbidden") let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &forbidden_script, - AskForApproval::OnRequest, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &forbidden_script, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -790,17 +836,18 @@ prefix_rule( let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &[ + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &[ "rm".to_string(), "-rf".to_string(), "/some/important/folder".to_string(), ], - AskForApproval::OnRequest, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -823,13 +870,14 @@ prefix_rule( let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::OnRequest, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -853,13 +901,14 @@ prefix_rule( let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::Never, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::Never, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -876,13 +925,14 @@ prefix_rule( let manager = ExecPolicyManager::default(); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -894,6 +944,40 @@ prefix_rule( ); } + #[tokio::test] + async fn request_rule_uses_prefix_rule() { + let command = vec![ + "cargo".to_string(), + "install".to_string(), + "cargo-insta".to_string(), + ]; + let manager = ExecPolicyManager::default(); + let mut features = Features::with_defaults(); + features.enable(Feature::RequestRule); + + let requirement = manager + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]), + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ + "cargo".to_string(), + "install".to_string(), + ])), + } + ); + } + #[tokio::test] async fn heuristics_apply_when_other_commands_match_policy() { let policy_src = r#"prefix_rule(pattern=["apple"], decision="allow")"#; @@ -910,13 +994,14 @@ prefix_rule( assert_eq!( ExecPolicyManager::new(policy) - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await, ExecApprovalRequirement::NeedsApproval { reason: None, @@ -984,13 +1069,14 @@ prefix_rule( let manager = ExecPolicyManager::default(); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1011,13 +1097,14 @@ prefix_rule( let manager = ExecPolicyManager::default(); let requirement = manager - .create_exec_approval_requirement_for_command( - &features, - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1041,13 +1128,14 @@ prefix_rule( let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::OnRequest, - &SandboxPolicy::DangerFullAccess, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1068,13 +1156,14 @@ prefix_rule( ]; let manager = ExecPolicyManager::default(); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1106,13 +1195,14 @@ prefix_rule( assert_eq!( ExecPolicyManager::new(policy) - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::UnlessTrusted, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await, ExecApprovalRequirement::NeedsApproval { reason: None, @@ -1129,13 +1219,14 @@ prefix_rule( let manager = ExecPolicyManager::default(); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::OnRequest, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1159,13 +1250,14 @@ prefix_rule( let manager = ExecPolicyManager::new(policy); let requirement = manager - .create_exec_approval_requirement_for_command( - &Features::with_defaults(), - &command, - AskForApproval::OnRequest, - &SandboxPolicy::ReadOnly, - SandboxPermissions::UseDefault, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) .await; assert_eq!( @@ -1226,13 +1318,14 @@ prefix_rule( assert_eq!( expected_req, policy - .create_exec_approval_requirement_for_command( - &features, - &sneaky_command, - AskForApproval::OnRequest, - &SandboxPolicy::ReadOnly, - permissions, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &sneaky_command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: permissions, + prefix_rule: None, + }) .await, "{pwsh_approval_reason}" ); @@ -1249,13 +1342,14 @@ prefix_rule( ]))), }, policy - .create_exec_approval_requirement_for_command( - &features, - &dangerous_command, - AskForApproval::OnRequest, - &SandboxPolicy::ReadOnly, - permissions, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &dangerous_command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: permissions, + prefix_rule: None, + }) .await, r#"On all platforms, a forbidden command should require approval (unless AskForApproval::Never is specified)."# @@ -1268,13 +1362,14 @@ prefix_rule( reason: "`rm -rf /important/data` rejected: blocked by policy".to_string(), }, policy - .create_exec_approval_requirement_for_command( - &features, - &dangerous_command, - AskForApproval::Never, - &SandboxPolicy::ReadOnly, - permissions, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &dangerous_command, + approval_policy: AskForApproval::Never, + sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_permissions: permissions, + prefix_rule: None, + }) .await, r#"On all platforms, a forbidden command should require approval (unless AskForApproval::Never is specified)."# diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 7fde52687c..26798c29d9 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -89,6 +89,8 @@ pub enum Feature { WebSearchCached, /// Gate the execpolicy enforcement for shell/unified exec. ExecPolicy, + /// Allow the model to request approval and propose exec rules. + RequestRule, /// Enable Windows sandbox (restricted token) on Windows. WindowsSandbox, /// Use the elevated Windows sandbox pipeline (setup + runner). @@ -99,6 +101,8 @@ pub enum Feature { RemoteModels, /// Experimental shell snapshotting. ShellSnapshot, + /// Persist rollout metadata to a local SQLite database. + Sqlite, /// Append additional AGENTS.md guidance to user instructions. ChildAgentsMd, /// Enforce UTF8 output in Powershell. @@ -109,6 +113,8 @@ pub enum Feature { Collab, /// Enable connectors (apps). Connectors, + /// Allow prompting and installing missing MCP dependencies. + SkillMcpDependencyInstall, /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. Steer, /// Enable collaboration modes (Plan, Code, Pair Programming, Execute). @@ -142,6 +148,8 @@ impl Feature { pub struct LegacyFeatureUsage { pub alias: String, pub feature: Feature, + pub summary: String, + pub details: Option, } /// Holds the effective set of enabled features. @@ -198,9 +206,12 @@ impl Features { } pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) { + let (summary, details) = legacy_usage_notice(alias, feature); self.legacy_usages.insert(LegacyFeatureUsage { alias: alias.to_string(), feature, + summary, + details, }); } @@ -211,10 +222,8 @@ impl Features { self.record_legacy_usage_force(alias, feature); } - pub fn legacy_feature_usages(&self) -> impl Iterator + '_ { - self.legacy_usages - .iter() - .map(|usage| (usage.alias.as_str(), usage.feature)) + pub fn legacy_feature_usages(&self) -> impl Iterator + '_ { + self.legacy_usages.iter() } pub fn emit_metrics(&self, otel: &OtelManager) { @@ -235,6 +244,21 @@ impl Features { /// Apply a table of key -> bool toggles (e.g. from TOML). pub fn apply_map(&mut self, m: &BTreeMap) { for (k, v) in m { + match k.as_str() { + "web_search_request" => { + self.record_legacy_usage_force( + "features.web_search_request", + Feature::WebSearchRequest, + ); + } + "web_search_cached" => { + self.record_legacy_usage_force( + "features.web_search_cached", + Feature::WebSearchCached, + ); + } + _ => {} + } match feature_for_key(k) { Some(feat) => { if k != feat.key() { @@ -295,6 +319,42 @@ impl Features { } } +fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option) { + let canonical = feature.key(); + match feature { + Feature::WebSearchRequest | Feature::WebSearchCached => { + let label = match alias { + "web_search" => "[features].web_search", + "tools.web_search" => "[tools].web_search", + "features.web_search_request" | "web_search_request" => { + "[features].web_search_request" + } + "features.web_search_cached" | "web_search_cached" => { + "[features].web_search_cached" + } + _ => alias, + }; + let summary = format!("`{label}` is deprecated. Use `web_search` instead."); + (summary, Some(web_search_details().to_string())) + } + _ => { + let summary = format!("`{alias}` is deprecated. Use `[features].{canonical}` instead."); + let details = if alias == canonical { + None + } else { + Some(format!( + "Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://github.com/openai/codex/blob/main/docs/config.md#feature-flags for details." + )) + }; + (summary, details) + } + } +} + +fn web_search_details() -> &'static str { + "Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` in config.toml." +} + /// Keys accepted in `[features]` tables. fn feature_for_key(key: &str) -> Option { for spec in FEATURES { @@ -343,13 +403,13 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::WebSearchRequest, key: "web_search_request", - stage: Stage::Stable, + stage: Stage::Deprecated, default_enabled: false, }, FeatureSpec { id: Feature::WebSearchCached, key: "web_search_cached", - stage: Stage::UnderDevelopment, + stage: Stage::Deprecated, default_enabled: false, }, // Experimental program. Rendered in the `/experimental` menu for users. @@ -373,6 +433,12 @@ pub const FEATURES: &[FeatureSpec] = &[ }, default_enabled: false, }, + FeatureSpec { + id: Feature::Sqlite, + key: "sqlite", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::ChildAgentsMd, key: "child_agents_md", @@ -391,6 +457,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: true, }, + FeatureSpec { + id: Feature::RequestRule, + key: "request_rule", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::WindowsSandbox, key: "experimental_windows_sandbox", @@ -434,8 +506,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::EnableRequestCompression, key: "enable_request_compression", - stage: Stage::UnderDevelopment, - default_enabled: false, + stage: Stage::Stable, + default_enabled: true, }, FeatureSpec { id: Feature::Collab, @@ -449,6 +521,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::SkillMcpDependencyInstall, + key: "skill_mcp_dependency_install", + stage: Stage::Stable, + default_enabled: true, + }, FeatureSpec { id: Feature::Steer, key: "steer", diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 53b5b7aa80..61f1442c78 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -92,6 +92,7 @@ pub mod shell; pub mod shell_snapshot; pub mod skills; pub mod spawn; +pub mod state_db; pub mod terminal; mod tools; pub mod turn_diff_tracker; @@ -128,9 +129,6 @@ pub use exec_policy::check_execpolicy_for_warnings; pub use exec_policy::load_exec_policy; pub use file_watcher::FileWatcherEvent; pub use safety::get_platform_sandbox; -pub use safety::is_windows_elevated_sandbox_enabled; -pub use safety::set_windows_elevated_sandbox_enabled; -pub use safety::set_windows_sandbox_enabled; pub use tools::spec::parse_tool_input_schema; // Re-export the protocol types from the standalone `codex-protocol` crate so existing // `codex_core::protocol::...` references continue to work across the workspace. diff --git a/codex-rs/core/src/mcp/auth.rs b/codex-rs/core/src/mcp/auth.rs index e321a857bb..f095c930dc 100644 --- a/codex-rs/core/src/mcp/auth.rs +++ b/codex-rs/core/src/mcp/auth.rs @@ -4,12 +4,53 @@ use anyhow::Result; use codex_protocol::protocol::McpAuthStatus; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_rmcp_client::determine_streamable_http_auth_status; +use codex_rmcp_client::supports_oauth_login; use futures::future::join_all; use tracing::warn; use crate::config::types::McpServerConfig; use crate::config::types::McpServerTransportConfig; +#[derive(Debug, Clone)] +pub struct McpOAuthLoginConfig { + pub url: String, + pub http_headers: Option>, + pub env_http_headers: Option>, +} + +#[derive(Debug)] +pub enum McpOAuthLoginSupport { + Supported(McpOAuthLoginConfig), + Unsupported, + Unknown(anyhow::Error), +} + +pub async fn oauth_login_support(transport: &McpServerTransportConfig) -> McpOAuthLoginSupport { + let McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var, + http_headers, + env_http_headers, + } = transport + else { + return McpOAuthLoginSupport::Unsupported; + }; + + if bearer_token_env_var.is_some() { + return McpOAuthLoginSupport::Unsupported; + } + + match supports_oauth_login(url).await { + Ok(true) => McpOAuthLoginSupport::Supported(McpOAuthLoginConfig { + url: url.clone(), + http_headers: http_headers.clone(), + env_http_headers: env_http_headers.clone(), + }), + Ok(false) => McpOAuthLoginSupport::Unsupported, + Err(err) => McpOAuthLoginSupport::Unknown(err), + } +} + #[derive(Debug, Clone)] pub struct McpAuthStatusEntry { pub config: McpServerConfig, diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 1bb08a75be..fa1de0f045 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -1,4 +1,8 @@ pub mod auth; +mod skill_dependencies; + +pub(crate) use skill_dependencies::maybe_prompt_and_install_mcp_dependencies; + use std::collections::HashMap; use std::env; use std::path::PathBuf; diff --git a/codex-rs/core/src/mcp/skill_dependencies.rs b/codex-rs/core/src/mcp/skill_dependencies.rs new file mode 100644 index 0000000000..c295a96863 --- /dev/null +++ b/codex-rs/core/src/mcp/skill_dependencies.rs @@ -0,0 +1,518 @@ +use std::collections::HashMap; +use std::collections::HashSet; + +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::request_user_input::RequestUserInputArgs; +use codex_protocol::request_user_input::RequestUserInputQuestion; +use codex_protocol::request_user_input::RequestUserInputQuestionOption; +use codex_protocol::request_user_input::RequestUserInputResponse; +use codex_rmcp_client::perform_oauth_login; +use tokio_util::sync::CancellationToken; +use tracing::warn; + +use super::auth::McpOAuthLoginSupport; +use super::auth::oauth_login_support; +use super::effective_mcp_servers; +use crate::codex::Session; +use crate::codex::TurnContext; +use crate::config::Config; +use crate::config::edit::ConfigEditsBuilder; +use crate::config::load_global_mcp_servers; +use crate::config::types::McpServerConfig; +use crate::config::types::McpServerTransportConfig; +use crate::default_client::is_first_party_originator; +use crate::default_client::originator; +use crate::features::Feature; +use crate::skills::SkillMetadata; +use crate::skills::model::SkillToolDependency; + +const SKILL_MCP_DEPENDENCY_PROMPT_ID: &str = "skill_mcp_dependency_install"; +const MCP_DEPENDENCY_OPTION_INSTALL: &str = "Install"; +const MCP_DEPENDENCY_OPTION_SKIP: &str = "Continue anyway"; + +fn is_full_access_mode(turn_context: &TurnContext) -> bool { + matches!(turn_context.approval_policy, AskForApproval::Never) + && matches!( + turn_context.sandbox_policy, + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + ) +} + +fn format_missing_mcp_dependencies(missing: &HashMap) -> String { + let mut names = missing.keys().cloned().collect::>(); + names.sort(); + names.join(", ") +} + +async fn filter_prompted_mcp_dependencies( + sess: &Session, + missing: &HashMap, +) -> HashMap { + let prompted = sess.mcp_dependency_prompted().await; + if prompted.is_empty() { + return missing.clone(); + } + + missing + .iter() + .filter(|(name, config)| !prompted.contains(&canonical_mcp_server_key(name, config))) + .map(|(name, config)| (name.clone(), config.clone())) + .collect() +} + +async fn should_install_mcp_dependencies( + sess: &Session, + turn_context: &TurnContext, + missing: &HashMap, + cancellation_token: &CancellationToken, +) -> bool { + if is_full_access_mode(turn_context) { + return true; + } + + let server_list = format_missing_mcp_dependencies(missing); + let question = RequestUserInputQuestion { + id: SKILL_MCP_DEPENDENCY_PROMPT_ID.to_string(), + header: "Install MCP servers?".to_string(), + question: format!( + "The following MCP servers are required by the selected skills but are not installed yet: {server_list}. Install them now?" + ), + is_other: false, + options: Some(vec![ + RequestUserInputQuestionOption { + label: MCP_DEPENDENCY_OPTION_INSTALL.to_string(), + description: + "Install and enable the missing MCP servers in your global config." + .to_string(), + }, + RequestUserInputQuestionOption { + label: MCP_DEPENDENCY_OPTION_SKIP.to_string(), + description: "Skip installation for now and do not show again for these MCP servers in this session." + .to_string(), + }, + ]), + }; + let args = RequestUserInputArgs { + questions: vec![question], + }; + let sub_id = &turn_context.sub_id; + let call_id = format!("mcp-deps-{sub_id}"); + let response_fut = sess.request_user_input(turn_context, call_id, args); + let response = tokio::select! { + biased; + _ = cancellation_token.cancelled() => { + let empty = RequestUserInputResponse { + answers: HashMap::new(), + }; + sess.notify_user_input_response(sub_id, empty.clone()).await; + empty + } + response = response_fut => response.unwrap_or_else(|| RequestUserInputResponse { + answers: HashMap::new(), + }), + }; + + let install = response + .answers + .get(SKILL_MCP_DEPENDENCY_PROMPT_ID) + .is_some_and(|answer| { + answer + .answers + .iter() + .any(|entry| entry == MCP_DEPENDENCY_OPTION_INSTALL) + }); + + let prompted_keys = missing + .iter() + .map(|(name, config)| canonical_mcp_server_key(name, config)); + sess.record_mcp_dependency_prompted(prompted_keys).await; + + install +} + +pub(crate) async fn maybe_prompt_and_install_mcp_dependencies( + sess: &Session, + turn_context: &TurnContext, + cancellation_token: &CancellationToken, + mentioned_skills: &[SkillMetadata], +) { + let originator_value = originator().value; + if !is_first_party_originator(originator_value.as_str()) { + // Only support first-party clients for now. + return; + } + + let config = turn_context.client.config(); + if mentioned_skills.is_empty() || !config.features.enabled(Feature::SkillMcpDependencyInstall) { + return; + } + + let installed = config.mcp_servers.get().clone(); + let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed); + if missing.is_empty() { + return; + } + + let unprompted_missing = filter_prompted_mcp_dependencies(sess, &missing).await; + if unprompted_missing.is_empty() { + return; + } + + if should_install_mcp_dependencies(sess, turn_context, &unprompted_missing, cancellation_token) + .await + { + maybe_install_mcp_dependencies(sess, turn_context, config.as_ref(), mentioned_skills).await; + } +} + +pub(crate) async fn maybe_install_mcp_dependencies( + sess: &Session, + turn_context: &TurnContext, + config: &Config, + mentioned_skills: &[SkillMetadata], +) { + if mentioned_skills.is_empty() || !config.features.enabled(Feature::SkillMcpDependencyInstall) { + return; + } + + let codex_home = config.codex_home.clone(); + let installed = config.mcp_servers.get().clone(); + let missing = collect_missing_mcp_dependencies(mentioned_skills, &installed); + if missing.is_empty() { + return; + } + + let mut servers = match load_global_mcp_servers(&codex_home).await { + Ok(servers) => servers, + Err(err) => { + warn!("failed to load MCP servers while installing skill dependencies: {err}"); + return; + } + }; + + let mut updated = false; + let mut added = Vec::new(); + for (name, config) in missing { + if servers.contains_key(&name) { + continue; + } + servers.insert(name.clone(), config.clone()); + added.push((name, config)); + updated = true; + } + + if !updated { + return; + } + + if let Err(err) = ConfigEditsBuilder::new(&codex_home) + .replace_mcp_servers(&servers) + .apply() + .await + { + warn!("failed to persist MCP dependencies for mentioned skills: {err}"); + return; + } + + for (name, server_config) in added { + let oauth_config = match oauth_login_support(&server_config.transport).await { + McpOAuthLoginSupport::Supported(config) => config, + McpOAuthLoginSupport::Unsupported => continue, + McpOAuthLoginSupport::Unknown(err) => { + warn!("MCP server may or may not require login for dependency {name}: {err}"); + continue; + } + }; + + sess.notify_background_event( + turn_context, + format!( + "Authenticating MCP {name}... Follow instructions in your browser if prompted." + ), + ) + .await; + + if let Err(err) = perform_oauth_login( + &name, + &oauth_config.url, + config.mcp_oauth_credentials_store_mode, + oauth_config.http_headers, + oauth_config.env_http_headers, + &[], + config.mcp_oauth_callback_port, + ) + .await + { + warn!("failed to login to MCP dependency {name}: {err}"); + } + } + + // Refresh from the effective merged MCP map (global + repo + managed) and + // overlay the updated global servers so we don't drop repo-scoped servers. + let auth = sess.services.auth_manager.auth().await; + let mut refresh_servers = effective_mcp_servers(config, auth.as_ref()); + for (name, server_config) in &servers { + refresh_servers + .entry(name.clone()) + .or_insert_with(|| server_config.clone()); + } + sess.refresh_mcp_servers_now( + turn_context, + refresh_servers, + config.mcp_oauth_credentials_store_mode, + ) + .await; +} + +fn canonical_mcp_key(transport: &str, identifier: &str, fallback: &str) -> String { + let identifier = identifier.trim(); + if identifier.is_empty() { + fallback.to_string() + } else { + format!("mcp__{transport}__{identifier}") + } +} + +fn canonical_mcp_server_key(name: &str, config: &McpServerConfig) -> String { + match &config.transport { + McpServerTransportConfig::Stdio { command, .. } => { + canonical_mcp_key("stdio", command, name) + } + McpServerTransportConfig::StreamableHttp { url, .. } => { + canonical_mcp_key("streamable_http", url, name) + } + } +} + +fn canonical_mcp_dependency_key(dependency: &SkillToolDependency) -> Result { + let transport = dependency.transport.as_deref().unwrap_or("streamable_http"); + if transport.eq_ignore_ascii_case("streamable_http") { + let url = dependency + .url + .as_ref() + .ok_or_else(|| "missing url for streamable_http dependency".to_string())?; + return Ok(canonical_mcp_key("streamable_http", url, &dependency.value)); + } + if transport.eq_ignore_ascii_case("stdio") { + let command = dependency + .command + .as_ref() + .ok_or_else(|| "missing command for stdio dependency".to_string())?; + return Ok(canonical_mcp_key("stdio", command, &dependency.value)); + } + Err(format!("unsupported transport {transport}")) +} + +pub(crate) fn collect_missing_mcp_dependencies( + mentioned_skills: &[SkillMetadata], + installed: &HashMap, +) -> HashMap { + let mut missing = HashMap::new(); + let installed_keys: HashSet = installed + .iter() + .map(|(name, config)| canonical_mcp_server_key(name, config)) + .collect(); + let mut seen_canonical_keys = HashSet::new(); + + for skill in mentioned_skills { + let Some(dependencies) = skill.dependencies.as_ref() else { + continue; + }; + + for tool in &dependencies.tools { + if !tool.r#type.eq_ignore_ascii_case("mcp") { + continue; + } + let dependency_key = match canonical_mcp_dependency_key(tool) { + Ok(key) => key, + Err(err) => { + let dependency = tool.value.as_str(); + let skill_name = skill.name.as_str(); + warn!( + "unable to auto-install MCP dependency {dependency} for skill {skill_name}: {err}", + ); + continue; + } + }; + if installed_keys.contains(&dependency_key) + || seen_canonical_keys.contains(&dependency_key) + { + continue; + } + + let config = match mcp_dependency_to_server_config(tool) { + Ok(config) => config, + Err(err) => { + let dependency = dependency_key.as_str(); + let skill_name = skill.name.as_str(); + warn!( + "unable to auto-install MCP dependency {dependency} for skill {skill_name}: {err}", + ); + continue; + } + }; + + missing.insert(tool.value.clone(), config); + seen_canonical_keys.insert(dependency_key); + } + } + + missing +} + +fn mcp_dependency_to_server_config( + dependency: &SkillToolDependency, +) -> Result { + let transport = dependency.transport.as_deref().unwrap_or("streamable_http"); + if transport.eq_ignore_ascii_case("streamable_http") { + let url = dependency + .url + .as_ref() + .ok_or_else(|| "missing url for streamable_http dependency".to_string())?; + return Ok(McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url: url.clone(), + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + enabled: true, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + }); + } + + if transport.eq_ignore_ascii_case("stdio") { + let command = dependency + .command + .as_ref() + .ok_or_else(|| "missing command for stdio dependency".to_string())?; + return Ok(McpServerConfig { + transport: McpServerTransportConfig::Stdio { + command: command.clone(), + args: Vec::new(), + env: None, + env_vars: Vec::new(), + cwd: None, + }, + enabled: true, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + }); + } + + Err(format!("unsupported transport {transport}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::skills::model::SkillDependencies; + use codex_protocol::protocol::SkillScope; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + + fn skill_with_tools(tools: Vec) -> SkillMetadata { + SkillMetadata { + name: "skill".to_string(), + description: "skill".to_string(), + short_description: None, + interface: None, + dependencies: Some(SkillDependencies { tools }), + path: PathBuf::from("skill"), + scope: SkillScope::User, + } + } + + #[test] + fn collect_missing_respects_canonical_installed_key() { + let url = "https://example.com/mcp".to_string(); + let skills = vec![skill_with_tools(vec![SkillToolDependency { + r#type: "mcp".to_string(), + value: "github".to_string(), + description: None, + transport: Some("streamable_http".to_string()), + command: None, + url: Some(url.clone()), + }])]; + let installed = HashMap::from([( + "alias".to_string(), + McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + enabled: true, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + }, + )]); + + assert_eq!( + collect_missing_mcp_dependencies(&skills, &installed), + HashMap::new() + ); + } + + #[test] + fn collect_missing_dedupes_by_canonical_key_but_preserves_original_name() { + let url = "https://example.com/one".to_string(); + let skills = vec![skill_with_tools(vec![ + SkillToolDependency { + r#type: "mcp".to_string(), + value: "alias-one".to_string(), + description: None, + transport: Some("streamable_http".to_string()), + command: None, + url: Some(url.clone()), + }, + SkillToolDependency { + r#type: "mcp".to_string(), + value: "alias-two".to_string(), + description: None, + transport: Some("streamable_http".to_string()), + command: None, + url: Some(url.clone()), + }, + ])]; + + let expected = HashMap::from([( + "alias-one".to_string(), + McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + enabled: true, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + }, + )]); + + assert_eq!( + collect_missing_mcp_dependencies(&skills, &HashMap::new()), + expected + ); + } +} diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index c07c144c11..5d8d0d2937 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -259,9 +259,7 @@ pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo { truncation_policy: TruncationPolicyConfig::tokens(10_000), context_window: Some(CONTEXT_WINDOW_272K), ) - } else if (slug.starts_with("gpt-5.2") || slug.starts_with("boomslang")) - && !slug.contains("codex") - { + } else if slug.starts_with("gpt-5.2") || slug.starts_with("boomslang") { model_info!( slug, apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), @@ -276,7 +274,7 @@ pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo { context_window: Some(CONTEXT_WINDOW_272K), supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh_non_codex(), ) - } else if slug.starts_with("gpt-5.1") && !slug.contains("codex") { + } else if slug.starts_with("gpt-5.1") { model_info!( slug, apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index 9609f366b2..edff86606b 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use std::cmp::Reverse; use std::ffi::OsStr; use std::io::{self}; @@ -7,8 +8,6 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; - -use async_trait::async_trait; use time::OffsetDateTime; use time::PrimitiveDateTime; use time::format_description::FormatItem; @@ -19,7 +18,9 @@ use uuid::Uuid; use super::ARCHIVED_SESSIONS_SUBDIR; use super::SESSIONS_SUBDIR; use crate::protocol::EventMsg; +use crate::state_db; use codex_file_search as file_search; +use codex_protocol::ThreadId; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SessionMetaLine; @@ -794,7 +795,7 @@ async fn collect_rollout_day_files( Ok(day_files) } -fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uuid)> { +pub(crate) fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uuid)> { // Expected: rollout-YYYY-MM-DDThh-mm-ss-.jsonl let core = name.strip_prefix("rollout-")?.strip_suffix(".jsonl")?; @@ -1093,11 +1094,39 @@ async fn find_thread_path_by_id_str_in_subdir( ) .map_err(|e| io::Error::other(format!("file search failed: {e}")))?; - Ok(results + let found = results .matches .into_iter() .next() - .map(|m| root.join(m.path))) + .map(|m| root.join(m.path)); + + // Checking if DB is at parity. + // TODO(jif): sqlite migration phase 1 + let archived_only = match subdir { + SESSIONS_SUBDIR => Some(false), + ARCHIVED_SESSIONS_SUBDIR => Some(true), + _ => None, + }; + let state_db_ctx = state_db::open_if_present(codex_home, "").await; + if let Some(state_db_ctx) = state_db_ctx.as_deref() + && let Ok(thread_id) = ThreadId::from_string(id_str) + { + let db_path = state_db::find_rollout_path_by_id( + Some(state_db_ctx), + thread_id, + archived_only, + "find_path_query", + ) + .await; + let canonical_path = found.as_deref(); + if db_path.as_deref() != canonical_path { + tracing::warn!( + "state db path mismatch for thread {thread_id:?}: canonical={canonical_path:?} db={db_path:?}" + ); + state_db::record_discrepancy("find_thread_path_by_id_str_in_subdir", "path_mismatch"); + } + } + Ok(found) } /// Locate a recorded thread rollout file by its UUID string using the existing diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs new file mode 100644 index 0000000000..32d13ebde2 --- /dev/null +++ b/codex-rs/core/src/rollout/metadata.rs @@ -0,0 +1,342 @@ +use crate::config::Config; +use crate::rollout; +use crate::rollout::list::parse_timestamp_uuid_from_filename; +use crate::rollout::recorder::RolloutRecorder; +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::Timelike; +use chrono::Utc; +use codex_otel::OtelManager; +use codex_protocol::ThreadId; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionMetaLine; +use codex_protocol::protocol::SessionSource; +use codex_state::BackfillStats; +use codex_state::DB_ERROR_METRIC; +use codex_state::ExtractionOutcome; +use codex_state::ThreadMetadataBuilder; +use codex_state::apply_rollout_item; +use std::cmp::Reverse; +use std::path::Path; +use std::path::PathBuf; +use tracing::warn; + +const ROLLOUT_PREFIX: &str = "rollout-"; +const ROLLOUT_SUFFIX: &str = ".jsonl"; + +pub(crate) fn builder_from_session_meta( + session_meta: &SessionMetaLine, + rollout_path: &Path, +) -> Option { + let created_at = parse_timestamp_to_utc(session_meta.meta.timestamp.as_str())?; + let mut builder = ThreadMetadataBuilder::new( + session_meta.meta.id, + rollout_path.to_path_buf(), + created_at, + session_meta.meta.source.clone(), + ); + builder.model_provider = session_meta.meta.model_provider.clone(); + builder.cwd = session_meta.meta.cwd.clone(); + builder.sandbox_policy = SandboxPolicy::ReadOnly; + builder.approval_mode = AskForApproval::OnRequest; + if let Some(git) = session_meta.git.as_ref() { + builder.git_sha = git.commit_hash.clone(); + builder.git_branch = git.branch.clone(); + builder.git_origin_url = git.repository_url.clone(); + } + Some(builder) +} + +pub(crate) fn builder_from_items( + items: &[RolloutItem], + rollout_path: &Path, +) -> Option { + if let Some(session_meta) = items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line), + RolloutItem::ResponseItem(_) + | RolloutItem::Compacted(_) + | RolloutItem::TurnContext(_) + | RolloutItem::EventMsg(_) => None, + }) && let Some(builder) = builder_from_session_meta(session_meta, rollout_path) + { + return Some(builder); + } + + let file_name = rollout_path.file_name()?.to_str()?; + if !file_name.starts_with(ROLLOUT_PREFIX) || !file_name.ends_with(ROLLOUT_SUFFIX) { + return None; + } + let (created_ts, uuid) = parse_timestamp_uuid_from_filename(file_name)?; + let created_at = + DateTime::::from_timestamp(created_ts.unix_timestamp(), 0)?.with_nanosecond(0)?; + let id = ThreadId::from_string(&uuid.to_string()).ok()?; + Some(ThreadMetadataBuilder::new( + id, + rollout_path.to_path_buf(), + created_at, + SessionSource::default(), + )) +} + +pub(crate) async fn extract_metadata_from_rollout( + rollout_path: &Path, + default_provider: &str, + otel: Option<&OtelManager>, +) -> anyhow::Result { + let (items, _thread_id, parse_errors) = + RolloutRecorder::load_rollout_items(rollout_path).await?; + if items.is_empty() { + return Err(anyhow::anyhow!( + "empty session file: {}", + rollout_path.display() + )); + } + let builder = builder_from_items(items.as_slice(), rollout_path).ok_or_else(|| { + anyhow::anyhow!( + "rollout missing metadata builder: {}", + rollout_path.display() + ) + })?; + let mut metadata = builder.build(default_provider); + for item in &items { + apply_rollout_item(&mut metadata, item, default_provider); + } + if let Some(updated_at) = file_modified_time_utc(rollout_path).await { + metadata.updated_at = updated_at; + } + if parse_errors > 0 + && let Some(otel) = otel + { + otel.counter( + DB_ERROR_METRIC, + parse_errors as i64, + &[("stage", "extract_metadata_from_rollout")], + ); + } + Ok(ExtractionOutcome { + metadata, + parse_errors, + }) +} + +pub(crate) async fn backfill_sessions( + runtime: &codex_state::StateRuntime, + config: &Config, + otel: Option<&OtelManager>, +) -> BackfillStats { + let sessions_root = config.codex_home.join(rollout::SESSIONS_SUBDIR); + let archived_root = config.codex_home.join(rollout::ARCHIVED_SESSIONS_SUBDIR); + let mut rollout_paths: Vec<(PathBuf, bool)> = Vec::new(); + for (root, archived) in [(sessions_root, false), (archived_root, true)] { + if !tokio::fs::try_exists(&root).await.unwrap_or(false) { + continue; + } + match collect_rollout_paths(&root).await { + Ok(paths) => { + rollout_paths.extend(paths.into_iter().map(|path| (path, archived))); + } + Err(err) => { + warn!( + "failed to collect rollout paths under {}: {err}", + root.display() + ); + } + } + } + rollout_paths.sort_by_key(|(path, _archived)| { + let parsed = path + .file_name() + .and_then(|name| name.to_str()) + .and_then(parse_timestamp_uuid_from_filename) + .unwrap_or((time::OffsetDateTime::UNIX_EPOCH, uuid::Uuid::nil())); + (Reverse(parsed.0), Reverse(parsed.1)) + }); + let mut stats = BackfillStats { + scanned: 0, + upserted: 0, + failed: 0, + }; + for (path, archived) in rollout_paths { + stats.scanned = stats.scanned.saturating_add(1); + match extract_metadata_from_rollout(&path, config.model_provider_id.as_str(), otel).await { + Ok(outcome) => { + if outcome.parse_errors > 0 + && let Some(otel) = otel + { + otel.counter( + DB_ERROR_METRIC, + outcome.parse_errors as i64, + &[("stage", "backfill_sessions")], + ); + } + let mut metadata = outcome.metadata; + if archived && metadata.archived_at.is_none() { + let fallback_archived_at = metadata.updated_at; + metadata.archived_at = file_modified_time_utc(&path) + .await + .or(Some(fallback_archived_at)); + } + if let Err(err) = runtime.upsert_thread(&metadata).await { + stats.failed = stats.failed.saturating_add(1); + warn!("failed to upsert rollout {}: {err}", path.display()); + } else { + stats.upserted = stats.upserted.saturating_add(1); + } + } + Err(err) => { + stats.failed = stats.failed.saturating_add(1); + warn!("failed to extract rollout {}: {err}", path.display()); + } + } + } + stats +} + +async fn file_modified_time_utc(path: &Path) -> Option> { + let modified = tokio::fs::metadata(path).await.ok()?.modified().ok()?; + let updated_at: DateTime = modified.into(); + updated_at.with_nanosecond(0) +} + +fn parse_timestamp_to_utc(ts: &str) -> Option> { + const FILENAME_TS_FORMAT: &str = "%Y-%m-%dT%H-%M-%S"; + if let Ok(naive) = NaiveDateTime::parse_from_str(ts, FILENAME_TS_FORMAT) { + let dt = DateTime::::from_naive_utc_and_offset(naive, Utc); + return dt.with_nanosecond(0); + } + if let Ok(dt) = DateTime::parse_from_rfc3339(ts) { + return dt.with_timezone(&Utc).with_nanosecond(0); + } + None +} + +async fn collect_rollout_paths(root: &Path) -> std::io::Result> { + let mut stack = vec![root.to_path_buf()]; + let mut paths = Vec::new(); + while let Some(dir) = stack.pop() { + let mut read_dir = match tokio::fs::read_dir(&dir).await { + Ok(read_dir) => read_dir, + Err(err) => { + warn!("failed to read directory {}: {err}", dir.display()); + continue; + } + }; + while let Some(entry) = read_dir.next_entry().await? { + let path = entry.path(); + let file_type = entry.file_type().await?; + if file_type.is_dir() { + stack.push(path); + continue; + } + if !file_type.is_file() { + continue; + } + let file_name = entry.file_name(); + let Some(name) = file_name.to_str() else { + continue; + }; + if name.starts_with(ROLLOUT_PREFIX) && name.ends_with(ROLLOUT_SUFFIX) { + paths.push(path); + } + } + } + Ok(paths) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::DateTime; + use chrono::NaiveDateTime; + use chrono::Timelike; + use chrono::Utc; + use codex_protocol::ThreadId; + use codex_protocol::protocol::CompactedItem; + 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_state::ThreadMetadataBuilder; + use pretty_assertions::assert_eq; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + use uuid::Uuid; + + #[tokio::test] + async fn extract_metadata_from_rollout_uses_session_meta() { + let dir = tempdir().expect("tempdir"); + let uuid = Uuid::new_v4(); + let id = ThreadId::from_string(&uuid.to_string()).expect("thread id"); + let path = dir + .path() + .join(format!("rollout-2026-01-27T12-34-56-{uuid}.jsonl")); + + let session_meta = SessionMeta { + id, + forked_from_id: None, + timestamp: "2026-01-27T12:34:56Z".to_string(), + cwd: dir.path().to_path_buf(), + originator: "cli".to_string(), + cli_version: "0.0.0".to_string(), + source: SessionSource::default(), + model_provider: Some("openai".to_string()), + base_instructions: None, + }; + let session_meta_line = SessionMetaLine { + meta: session_meta, + git: None, + }; + let rollout_line = RolloutLine { + timestamp: "2026-01-27T12:34:56Z".to_string(), + item: RolloutItem::SessionMeta(session_meta_line.clone()), + }; + let json = serde_json::to_string(&rollout_line).expect("rollout json"); + let mut file = File::create(&path).expect("create rollout"); + writeln!(file, "{json}").expect("write rollout"); + + let outcome = extract_metadata_from_rollout(&path, "openai", None) + .await + .expect("extract"); + + let builder = + builder_from_session_meta(&session_meta_line, path.as_path()).expect("builder"); + let mut expected = builder.build("openai"); + apply_rollout_item(&mut expected, &rollout_line.item, "openai"); + expected.updated_at = file_modified_time_utc(&path).await.expect("mtime"); + + assert_eq!(outcome.metadata, expected); + assert_eq!(outcome.parse_errors, 0); + } + + #[test] + fn builder_from_items_falls_back_to_filename() { + let dir = tempdir().expect("tempdir"); + let uuid = Uuid::new_v4(); + let path = dir + .path() + .join(format!("rollout-2026-01-27T12-34-56-{uuid}.jsonl")); + let items = vec![RolloutItem::Compacted(CompactedItem { + message: "noop".to_string(), + replacement_history: None, + })]; + + let builder = builder_from_items(items.as_slice(), path.as_path()).expect("builder"); + let naive = NaiveDateTime::parse_from_str("2026-01-27T12-34-56", "%Y-%m-%dT%H-%M-%S") + .expect("timestamp"); + let created_at = DateTime::::from_naive_utc_and_offset(naive, Utc) + .with_nanosecond(0) + .expect("nanosecond"); + let expected = ThreadMetadataBuilder::new( + ThreadId::from_string(&uuid.to_string()).expect("thread id"), + path, + created_at, + SessionSource::default(), + ); + + assert_eq!(builder, expected); + } +} diff --git a/codex-rs/core/src/rollout/mod.rs b/codex-rs/core/src/rollout/mod.rs index fbddfecf35..cfc2d82d8d 100644 --- a/codex-rs/core/src/rollout/mod.rs +++ b/codex-rs/core/src/rollout/mod.rs @@ -9,6 +9,7 @@ pub const INTERACTIVE_SESSION_SOURCES: &[SessionSource] = pub(crate) mod error; pub mod list; +pub(crate) mod metadata; pub(crate) mod policy; pub mod recorder; pub(crate) mod truncation; diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 53425051cf..cc35850544 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -28,11 +28,14 @@ use super::list::ThreadSortKey; use super::list::ThreadsPage; use super::list::get_threads; use super::list::get_threads_in_root; +use super::metadata; use super::policy::is_persisted_response_item; use crate::config::Config; use crate::default_client::originator; use crate::git_info::collect_git_info; use crate::path_utils; +use crate::state_db; +use crate::state_db::StateDbHandle; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::ResumedHistory; use codex_protocol::protocol::RolloutItem; @@ -40,6 +43,7 @@ use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; +use codex_state::ThreadMetadataBuilder; /// Records all [`ResponseItem`]s for a session and flushes them to disk after /// every update. @@ -54,6 +58,7 @@ use codex_protocol::protocol::SessionSource; pub struct RolloutRecorder { tx: Sender, pub(crate) rollout_path: PathBuf, + state_db: Option, } #[derive(Clone)] @@ -111,7 +116,8 @@ impl RolloutRecorder { model_providers: Option<&[String]>, default_provider: &str, ) -> std::io::Result { - get_threads( + let stage = "list_threads"; + let page = get_threads( codex_home, page_size, cursor, @@ -120,7 +126,34 @@ impl RolloutRecorder { model_providers, default_provider, ) + .await?; + + // TODO(jif): drop after sqlite migration phase 1 + let state_db_ctx = state_db::open_if_present(codex_home, default_provider).await; + if let Some(db_ids) = state_db::list_thread_ids_db( + state_db_ctx.as_deref(), + codex_home, + page_size, + cursor, + sort_key, + allowed_sources, + model_providers, + false, + stage, + ) .await + { + if page.items.len() != db_ids.len() { + state_db::record_discrepancy(stage, "bad_len"); + return Ok(page); + } + for (id, item) in db_ids.iter().zip(page.items.iter()) { + if !item.path.display().to_string().contains(&id.to_string()) { + state_db::record_discrepancy(stage, "bad_id"); + } + } + } + Ok(page) } /// List archived threads (rollout files) under the archived sessions directory. @@ -133,8 +166,9 @@ impl RolloutRecorder { model_providers: Option<&[String]>, default_provider: &str, ) -> std::io::Result { + let stage = "list_archived_threads"; let root = codex_home.join(ARCHIVED_SESSIONS_SUBDIR); - get_threads_in_root( + let page = get_threads_in_root( root, page_size, cursor, @@ -146,7 +180,34 @@ impl RolloutRecorder { layout: ThreadListLayout::Flat, }, ) + .await?; + + // TODO(jif): drop after sqlite migration phase 1 + let state_db_ctx = state_db::open_if_present(codex_home, default_provider).await; + if let Some(db_ids) = state_db::list_thread_ids_db( + state_db_ctx.as_deref(), + codex_home, + page_size, + cursor, + sort_key, + allowed_sources, + model_providers, + true, + stage, + ) .await + { + if page.items.len() != db_ids.len() { + state_db::record_discrepancy(stage, "bad_len"); + return Ok(page); + } + for (id, item) in db_ids.iter().zip(page.items.iter()) { + if !item.path.display().to_string().contains(&id.to_string()) { + state_db::record_discrepancy(stage, "bad_id"); + } + } + } + Ok(page) } /// Find the newest recorded thread path, optionally filtering to a matching cwd. @@ -186,7 +247,12 @@ impl RolloutRecorder { /// Attempt to create a new [`RolloutRecorder`]. If the sessions directory /// cannot be created or the rollout file cannot be opened we return the /// error so the caller can decide whether to disable persistence. - pub async fn new(config: &Config, params: RolloutRecorderParams) -> std::io::Result { + pub async fn new( + config: &Config, + params: RolloutRecorderParams, + state_db_ctx: Option, + state_builder: Option, + ) -> std::io::Result { let (file, rollout_path, meta) = match params { RolloutRecorderParams::Create { conversation_id, @@ -246,9 +312,30 @@ impl RolloutRecorder { // Spawn a Tokio task that owns the file handle and performs async // writes. Using `tokio::fs::File` keeps everything on the async I/O // driver instead of blocking the runtime. - tokio::task::spawn(rollout_writer(file, rx, meta, cwd)); + tokio::task::spawn(rollout_writer( + file, + rx, + meta, + cwd, + rollout_path.clone(), + state_db_ctx.clone(), + state_builder, + config.model_provider_id.clone(), + )); - Ok(Self { tx, rollout_path }) + Ok(Self { + tx, + rollout_path, + state_db: state_db_ctx, + }) + } + + pub fn rollout_path(&self) -> &Path { + self.rollout_path.as_path() + } + + pub fn state_db(&self) -> Option { + self.state_db.clone() } pub(crate) async fn record_items(&self, items: &[RolloutItem]) -> std::io::Result<()> { @@ -281,7 +368,9 @@ impl RolloutRecorder { .map_err(|e| IoError::other(format!("failed waiting for rollout flush: {e}"))) } - pub async fn get_rollout_history(path: &Path) -> std::io::Result { + pub(crate) async fn load_rollout_items( + path: &Path, + ) -> std::io::Result<(Vec, Option, usize)> { info!("Resuming rollout from {path:?}"); let text = tokio::fs::read_to_string(path).await?; if text.trim().is_empty() { @@ -290,6 +379,7 @@ impl RolloutRecorder { let mut items: Vec = Vec::new(); let mut thread_id: Option = None; + let mut parse_errors = 0usize; for line in text.lines() { if line.trim().is_empty() { continue; @@ -298,6 +388,7 @@ impl RolloutRecorder { Ok(v) => v, Err(e) => { warn!("failed to parse line as JSON: {line:?}, error: {e}"); + parse_errors = parse_errors.saturating_add(1); continue; } }; @@ -328,15 +419,22 @@ impl RolloutRecorder { }, Err(e) => { warn!("failed to parse rollout line: {v:?}, error: {e}"); + parse_errors = parse_errors.saturating_add(1); } } } info!( - "Resumed rollout with {} items, thread ID: {:?}", + "Resumed rollout with {} items, thread ID: {:?}, parse errors: {}", items.len(), - thread_id + thread_id, + parse_errors, ); + Ok((items, thread_id, parse_errors)) + } + + pub async fn get_rollout_history(path: &Path) -> std::io::Result { + let (items, thread_id, _parse_errors) = Self::load_rollout_items(path).await?; let conversation_id = thread_id .ok_or_else(|| IoError::other("failed to parse thread ID from rollout file"))?; @@ -417,13 +515,21 @@ fn create_log_file(config: &Config, conversation_id: ThreadId) -> std::io::Resul }) } +#[allow(clippy::too_many_arguments)] async fn rollout_writer( file: tokio::fs::File, mut rx: mpsc::Receiver, mut meta: Option, cwd: std::path::PathBuf, + rollout_path: PathBuf, + state_db_ctx: Option, + mut state_builder: Option, + default_provider: String, ) -> std::io::Result<()> { let mut writer = JsonlWriter { file }; + if let Some(builder) = state_builder.as_mut() { + builder.rollout_path = rollout_path.clone(); + } // If we have a meta, collect git info asynchronously and write meta first if let Some(session_meta) = meta.take() { @@ -432,22 +538,50 @@ async fn rollout_writer( meta: session_meta, git: git_info, }; + if state_db_ctx.is_some() { + state_builder = + metadata::builder_from_session_meta(&session_meta_line, rollout_path.as_path()); + } // Write the SessionMeta as the first item in the file, wrapped in a rollout line - writer - .write_rollout_item(RolloutItem::SessionMeta(session_meta_line)) - .await?; + let rollout_item = RolloutItem::SessionMeta(session_meta_line); + writer.write_rollout_item(&rollout_item).await?; + state_db::reconcile_rollout( + state_db_ctx.as_deref(), + rollout_path.as_path(), + default_provider.as_str(), + state_builder.as_ref(), + std::slice::from_ref(&rollout_item), + ) + .await; } // Process rollout commands while let Some(cmd) = rx.recv().await { match cmd { RolloutCmd::AddItems(items) => { + let mut persisted_items = Vec::new(); for item in items { if is_persisted_response_item(&item) { - writer.write_rollout_item(item).await?; + writer.write_rollout_item(&item).await?; + persisted_items.push(item); } } + if persisted_items.is_empty() { + continue; + } + if let Some(builder) = state_builder.as_mut() { + builder.rollout_path = rollout_path.clone(); + } + state_db::apply_rollout_items( + state_db_ctx.as_deref(), + rollout_path.as_path(), + default_provider.as_str(), + state_builder.as_ref(), + persisted_items.as_slice(), + "rollout_writer", + ) + .await; } RolloutCmd::Flush { ack } => { // Ensure underlying file is flushed and then ack. @@ -470,8 +604,15 @@ struct JsonlWriter { file: tokio::fs::File, } +#[derive(serde::Serialize)] +struct RolloutLineRef<'a> { + timestamp: String, + #[serde(flatten)] + item: &'a RolloutItem, +} + impl JsonlWriter { - async fn write_rollout_item(&mut self, rollout_item: RolloutItem) -> std::io::Result<()> { + async fn write_rollout_item(&mut self, rollout_item: &RolloutItem) -> std::io::Result<()> { let timestamp_format: &[FormatItem] = format_description!( "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z" ); @@ -479,7 +620,7 @@ impl JsonlWriter { .format(timestamp_format) .map_err(|e| IoError::other(format!("failed to format timestamp: {e}")))?; - let line = RolloutLine { + let line = RolloutLineRef { timestamp, item: rollout_item, }; diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 601a5a8b81..47a12e029e 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -10,45 +10,7 @@ use crate::util::resolve_path; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; - -#[cfg(target_os = "windows")] -use std::sync::atomic::AtomicBool; -#[cfg(target_os = "windows")] -use std::sync::atomic::Ordering; - -#[cfg(target_os = "windows")] -static WINDOWS_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false); -#[cfg(target_os = "windows")] -static WINDOWS_ELEVATED_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false); - -#[cfg(target_os = "windows")] -pub fn set_windows_sandbox_enabled(enabled: bool) { - WINDOWS_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed); -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn set_windows_sandbox_enabled(_enabled: bool) {} - -#[cfg(target_os = "windows")] -pub fn set_windows_elevated_sandbox_enabled(enabled: bool) { - WINDOWS_ELEVATED_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed); -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn set_windows_elevated_sandbox_enabled(_enabled: bool) {} - -#[cfg(target_os = "windows")] -pub fn is_windows_elevated_sandbox_enabled() -> bool { - WINDOWS_ELEVATED_SANDBOX_ENABLED.load(Ordering::Relaxed) -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn is_windows_elevated_sandbox_enabled() -> bool { - false -} +use codex_protocol::config_types::WindowsSandboxLevel; #[derive(Debug, PartialEq)] pub enum SafetyCheck { @@ -67,6 +29,7 @@ pub fn assess_patch_safety( policy: AskForApproval, sandbox_policy: &SandboxPolicy, cwd: &Path, + windows_sandbox_level: WindowsSandboxLevel, ) -> SafetyCheck { if action.is_empty() { return SafetyCheck::Reject { @@ -104,7 +67,7 @@ pub fn assess_patch_safety( // Only auto‑approve when we can actually enforce a sandbox. Otherwise // fall back to asking the user because the patch may touch arbitrary // paths outside the project. - match get_platform_sandbox() { + match get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) { Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type, user_explicitly_approved: false, @@ -122,19 +85,17 @@ pub fn assess_patch_safety( } } -pub fn get_platform_sandbox() -> Option { +pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { if cfg!(target_os = "macos") { Some(SandboxType::MacosSeatbelt) } else if cfg!(target_os = "linux") { Some(SandboxType::LinuxSeccomp) } else if cfg!(target_os = "windows") { - #[cfg(target_os = "windows")] - { - if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) { - return Some(SandboxType::WindowsRestrictedToken); - } + if windows_sandbox_enabled { + Some(SandboxType::WindowsRestrictedToken) + } else { + None } - None } else { None } @@ -277,7 +238,13 @@ mod tests { }; assert_eq!( - assess_patch_safety(&add_inside, AskForApproval::OnRequest, &policy, &cwd), + assess_patch_safety( + &add_inside, + AskForApproval::OnRequest, + &policy, + &cwd, + WindowsSandboxLevel::Disabled + ), SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, user_explicitly_approved: false, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index a2c8ad1e31..fca7adda29 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -21,6 +21,7 @@ use crate::seatbelt::create_seatbelt_command_args; use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; +use codex_protocol::config_types::WindowsSandboxLevel; pub use codex_protocol::models::SandboxPermissions; use std::collections::HashMap; use std::path::Path; @@ -44,6 +45,7 @@ pub struct ExecEnv { pub env: HashMap, pub expiration: ExecExpiration, pub sandbox: SandboxType, + pub windows_sandbox_level: WindowsSandboxLevel, pub sandbox_permissions: SandboxPermissions, pub justification: Option, pub arg0: Option, @@ -76,19 +78,26 @@ impl SandboxManager { &self, policy: &SandboxPolicy, pref: SandboxablePreference, + windows_sandbox_level: WindowsSandboxLevel, ) -> SandboxType { match pref { SandboxablePreference::Forbid => SandboxType::None, SandboxablePreference::Require => { // Require a platform sandbox when available; on Windows this // respects the experimental_windows_sandbox feature. - crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None) + crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None) } SandboxablePreference::Auto => match policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { SandboxType::None } - _ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None), + _ => crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None), }, } } @@ -100,6 +109,7 @@ impl SandboxManager { sandbox: SandboxType, sandbox_policy_cwd: &Path, codex_linux_sandbox_exe: Option<&PathBuf>, + windows_sandbox_level: WindowsSandboxLevel, ) -> Result { let mut env = spec.env; if !policy.has_full_network_access() { @@ -160,6 +170,7 @@ impl SandboxManager { env, expiration: spec.expiration, sandbox, + windows_sandbox_level, sandbox_permissions: spec.sandbox_permissions, justification: spec.justification, arg0: arg0_override, diff --git a/codex-rs/core/src/skills/injection.rs b/codex-rs/core/src/skills/injection.rs index 9aa12d775c..65ec4dd510 100644 --- a/codex-rs/core/src/skills/injection.rs +++ b/codex-rs/core/src/skills/injection.rs @@ -2,7 +2,6 @@ use std::collections::HashSet; use std::path::PathBuf; use crate::instructions::SkillInstructions; -use crate::skills::SkillLoadOutcome; use crate::skills::SkillMetadata; use codex_otel::OtelManager; use codex_protocol::models::ResponseItem; @@ -16,20 +15,9 @@ pub(crate) struct SkillInjections { } pub(crate) async fn build_skill_injections( - inputs: &[UserInput], - skills: Option<&SkillLoadOutcome>, + mentioned_skills: &[SkillMetadata], otel: Option<&OtelManager>, ) -> SkillInjections { - if inputs.is_empty() { - return SkillInjections::default(); - } - - let Some(outcome) = skills else { - return SkillInjections::default(); - }; - - let mentioned_skills = - collect_explicit_skill_mentions(inputs, &outcome.skills, &outcome.disabled_paths); if mentioned_skills.is_empty() { return SkillInjections::default(); } @@ -42,15 +30,15 @@ pub(crate) async fn build_skill_injections( for skill in mentioned_skills { match fs::read_to_string(&skill.path).await { Ok(contents) => { - emit_skill_injected_metric(otel, &skill, "ok"); + emit_skill_injected_metric(otel, skill, "ok"); result.items.push(ResponseItem::from(SkillInstructions { - name: skill.name, + name: skill.name.clone(), path: skill.path.to_string_lossy().into_owned(), contents, })); } Err(err) => { - emit_skill_injected_metric(otel, &skill, "error"); + emit_skill_injected_metric(otel, skill, "error"); let message = format!( "Failed to load skill {name} at {path}: {err:#}", name = skill.name, @@ -76,23 +64,488 @@ fn emit_skill_injected_metric(otel: Option<&OtelManager>, skill: &SkillMetadata, ); } -fn collect_explicit_skill_mentions( +/// Collect explicitly mentioned skills from `$name` text mentions. +/// +/// Text inputs are scanned once to extract `$skill-name` tokens, then we iterate `skills` +/// in their existing order to preserve prior ordering semantics. +/// +/// Complexity: `O(S + T + N_t * S)` time, `O(S)` space, where: +/// `S` = number of skills, `T` = total text length, `N_t` = number of text inputs. +pub(crate) fn collect_explicit_skill_mentions( inputs: &[UserInput], skills: &[SkillMetadata], disabled_paths: &HashSet, ) -> Vec { let mut selected: Vec = Vec::new(); - let mut seen: HashSet = HashSet::new(); + let mut seen_names: HashSet = HashSet::new(); + let mut seen_paths: HashSet = HashSet::new(); for input in inputs { - if let UserInput::Skill { name, path } = input - && seen.insert(name.clone()) - && let Some(skill) = skills.iter().find(|s| s.name == *name && s.path == *path) - && !disabled_paths.contains(&skill.path) - { - selected.push(skill.clone()); + if let UserInput::Text { text, .. } = input { + let mentioned_names = extract_skill_mentions(text); + select_skills_from_mentions( + skills, + disabled_paths, + &mentioned_names, + &mut seen_names, + &mut seen_paths, + &mut selected, + ); } } selected } + +struct SkillMentions<'a> { + names: HashSet<&'a str>, + paths: HashSet<&'a str>, +} + +impl<'a> SkillMentions<'a> { + fn is_empty(&self) -> bool { + self.names.is_empty() && self.paths.is_empty() + } +} + +/// Extract `$skill-name` mentions from a single text input. +/// +/// Supports explicit resource links in the form `[$skill-name](resource path)`. When a +/// resource path is present, it is captured for exact path matching while also tracking +/// the name for fallback matching. +fn extract_skill_mentions(text: &str) -> SkillMentions<'_> { + let text_bytes = text.as_bytes(); + let mut mentioned_names: HashSet<&str> = HashSet::new(); + let mut mentioned_paths: HashSet<&str> = HashSet::new(); + + let mut index = 0; + while index < text_bytes.len() { + let byte = text_bytes[index]; + if byte == b'[' + && let Some((name, path, end_index)) = + parse_linked_skill_mention(text, text_bytes, index) + { + if !is_common_env_var(name) { + mentioned_names.insert(name); + mentioned_paths.insert(path); + } + index = end_index; + continue; + } + + if byte != b'$' { + index += 1; + continue; + } + + let name_start = index + 1; + let Some(first_name_byte) = text_bytes.get(name_start) else { + index += 1; + continue; + }; + if !is_skill_name_char(*first_name_byte) { + index += 1; + continue; + } + + let mut name_end = name_start + 1; + while let Some(next_byte) = text_bytes.get(name_end) + && is_skill_name_char(*next_byte) + { + name_end += 1; + } + + let name = &text[name_start..name_end]; + if !is_common_env_var(name) { + mentioned_names.insert(name); + } + index = name_end; + } + + SkillMentions { + names: mentioned_names, + paths: mentioned_paths, + } +} + +/// Select mentioned skills while preserving the order of `skills`. +fn select_skills_from_mentions( + skills: &[SkillMetadata], + disabled_paths: &HashSet, + mentions: &SkillMentions<'_>, + seen_names: &mut HashSet, + seen_paths: &mut HashSet, + selected: &mut Vec, +) { + if mentions.is_empty() { + return; + } + + for skill in skills { + if disabled_paths.contains(&skill.path) || seen_paths.contains(&skill.path) { + continue; + } + + let path_str = skill.path.to_string_lossy(); + if mentions.paths.contains(path_str.as_ref()) { + seen_paths.insert(skill.path.clone()); + seen_names.insert(skill.name.clone()); + selected.push(skill.clone()); + } + } + + for skill in skills { + if disabled_paths.contains(&skill.path) || seen_paths.contains(&skill.path) { + continue; + } + + if mentions.names.contains(skill.name.as_str()) && seen_names.insert(skill.name.clone()) { + seen_paths.insert(skill.path.clone()); + selected.push(skill.clone()); + } + } +} + +fn parse_linked_skill_mention<'a>( + text: &'a str, + text_bytes: &[u8], + start: usize, +) -> Option<(&'a str, &'a str, usize)> { + let dollar_index = start + 1; + if text_bytes.get(dollar_index) != Some(&b'$') { + return None; + } + + let name_start = dollar_index + 1; + let first_name_byte = text_bytes.get(name_start)?; + if !is_skill_name_char(*first_name_byte) { + return None; + } + + let mut name_end = name_start + 1; + while let Some(next_byte) = text_bytes.get(name_end) + && is_skill_name_char(*next_byte) + { + name_end += 1; + } + + if text_bytes.get(name_end) != Some(&b']') { + return None; + } + + let mut path_start = name_end + 1; + while let Some(next_byte) = text_bytes.get(path_start) + && next_byte.is_ascii_whitespace() + { + path_start += 1; + } + if text_bytes.get(path_start) != Some(&b'(') { + return None; + } + + let mut path_end = path_start + 1; + while let Some(next_byte) = text_bytes.get(path_end) + && *next_byte != b')' + { + path_end += 1; + } + if text_bytes.get(path_end) != Some(&b')') { + return None; + } + + let path = text[path_start + 1..path_end].trim(); + if path.is_empty() { + return None; + } + + let name = &text[name_start..name_end]; + Some((name, path, path_end + 1)) +} + +fn is_common_env_var(name: &str) -> bool { + let upper = name.to_ascii_uppercase(); + matches!( + upper.as_str(), + "PATH" + | "HOME" + | "USER" + | "SHELL" + | "PWD" + | "TMPDIR" + | "TEMP" + | "TMP" + | "LANG" + | "TERM" + | "XDG_CONFIG_HOME" + ) +} + +#[cfg(test)] +fn text_mentions_skill(text: &str, skill_name: &str) -> bool { + if skill_name.is_empty() { + return false; + } + + let text_bytes = text.as_bytes(); + let skill_bytes = skill_name.as_bytes(); + + for (index, byte) in text_bytes.iter().copied().enumerate() { + if byte != b'$' { + continue; + } + + let name_start = index + 1; + let Some(rest) = text_bytes.get(name_start..) else { + continue; + }; + if !rest.starts_with(skill_bytes) { + continue; + } + + let after_index = name_start + skill_bytes.len(); + let after = text_bytes.get(after_index).copied(); + if after.is_none_or(|b| !is_skill_name_char(b)) { + return true; + } + } + + false +} + +fn is_skill_name_char(byte: u8) -> bool { + matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-') +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use std::collections::HashSet; + + fn make_skill(name: &str, path: &str) -> SkillMetadata { + SkillMetadata { + name: name.to_string(), + description: format!("{name} skill"), + short_description: None, + interface: None, + dependencies: None, + path: PathBuf::from(path), + scope: codex_protocol::protocol::SkillScope::User, + } + } + + fn set<'a>(items: &'a [&'a str]) -> HashSet<&'a str> { + items.iter().copied().collect() + } + + fn assert_mentions(text: &str, expected_names: &[&str], expected_paths: &[&str]) { + let mentions = extract_skill_mentions(text); + assert_eq!(mentions.names, set(expected_names)); + assert_eq!(mentions.paths, set(expected_paths)); + } + + #[test] + fn text_mentions_skill_requires_exact_boundary() { + assert_eq!( + true, + text_mentions_skill("use $notion-research-doc please", "notion-research-doc") + ); + assert_eq!( + true, + text_mentions_skill("($notion-research-doc)", "notion-research-doc") + ); + assert_eq!( + true, + text_mentions_skill("$notion-research-doc.", "notion-research-doc") + ); + assert_eq!( + false, + text_mentions_skill("$notion-research-docs", "notion-research-doc") + ); + assert_eq!( + false, + text_mentions_skill("$notion-research-doc_extra", "notion-research-doc") + ); + } + + #[test] + fn text_mentions_skill_handles_end_boundary_and_near_misses() { + assert_eq!(true, text_mentions_skill("$alpha-skill", "alpha-skill")); + assert_eq!(false, text_mentions_skill("$alpha-skillx", "alpha-skill")); + assert_eq!( + true, + text_mentions_skill("$alpha-skillx and later $alpha-skill ", "alpha-skill") + ); + } + + #[test] + fn text_mentions_skill_handles_many_dollars_without_looping() { + let prefix = "$".repeat(256); + let text = format!("{prefix} not-a-mention"); + assert_eq!(false, text_mentions_skill(&text, "alpha-skill")); + } + + #[test] + fn extract_skill_mentions_handles_plain_and_linked_mentions() { + assert_mentions( + "use $alpha and [$beta](/tmp/beta)", + &["alpha", "beta"], + &["/tmp/beta"], + ); + } + + #[test] + fn extract_skill_mentions_skips_common_env_vars() { + assert_mentions("use $PATH and $alpha", &["alpha"], &[]); + assert_mentions("use [$HOME](/tmp/skill)", &[], &[]); + assert_mentions("use $XDG_CONFIG_HOME and $beta", &["beta"], &[]); + } + + #[test] + fn extract_skill_mentions_requires_link_syntax() { + assert_mentions("[beta](/tmp/beta)", &[], &[]); + assert_mentions("[$beta] /tmp/beta", &["beta"], &[]); + assert_mentions("[$beta]()", &["beta"], &[]); + } + + #[test] + fn extract_skill_mentions_trims_linked_paths_and_allows_spacing() { + assert_mentions("use [$beta] ( /tmp/beta )", &["beta"], &["/tmp/beta"]); + } + + #[test] + fn extract_skill_mentions_stops_at_non_name_chars() { + assert_mentions( + "use $alpha.skill and $beta_extra", + &["alpha", "beta_extra"], + &[], + ); + } + + #[test] + fn collect_explicit_skill_mentions_text_respects_skill_order() { + let alpha = make_skill("alpha-skill", "/tmp/alpha"); + let beta = make_skill("beta-skill", "/tmp/beta"); + let skills = vec![beta.clone(), alpha.clone()]; + let inputs = vec![UserInput::Text { + text: "first $alpha-skill then $beta-skill".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + // Text scanning should not change the previous selection ordering semantics. + assert_eq!(selected, vec![beta, alpha]); + } + + #[test] + fn collect_explicit_skill_mentions_ignores_structured_inputs() { + let alpha = make_skill("alpha-skill", "/tmp/alpha"); + let beta = make_skill("beta-skill", "/tmp/beta"); + let skills = vec![alpha.clone(), beta]; + let inputs = vec![ + UserInput::Text { + text: "please run $alpha-skill".to_string(), + text_elements: Vec::new(), + }, + UserInput::Skill { + name: "beta-skill".to_string(), + path: PathBuf::from("/tmp/beta"), + }, + ]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![alpha]); + } + + #[test] + fn collect_explicit_skill_mentions_dedupes_by_path() { + let alpha = make_skill("alpha-skill", "/tmp/alpha"); + let skills = vec![alpha.clone()]; + let inputs = vec![UserInput::Text { + text: "use [$alpha-skill](/tmp/alpha) and [$alpha-skill](/tmp/alpha)".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![alpha]); + } + + #[test] + fn collect_explicit_skill_mentions_dedupes_by_name() { + let alpha = make_skill("demo-skill", "/tmp/alpha"); + let beta = make_skill("demo-skill", "/tmp/beta"); + let skills = vec![alpha.clone(), beta]; + let inputs = vec![UserInput::Text { + text: "use $demo-skill and again $demo-skill".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![alpha]); + } + + #[test] + fn collect_explicit_skill_mentions_prefers_linked_path_over_name() { + let alpha = make_skill("demo-skill", "/tmp/alpha"); + let beta = make_skill("demo-skill", "/tmp/beta"); + let skills = vec![alpha, beta.clone()]; + let inputs = vec![UserInput::Text { + text: "use $demo-skill and [$demo-skill](/tmp/beta)".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![beta]); + } + + #[test] + fn collect_explicit_skill_mentions_falls_back_when_linked_path_disabled() { + let alpha = make_skill("demo-skill", "/tmp/alpha"); + let beta = make_skill("demo-skill", "/tmp/beta"); + let skills = vec![alpha, beta.clone()]; + let inputs = vec![UserInput::Text { + text: "use [$demo-skill](/tmp/alpha)".to_string(), + text_elements: Vec::new(), + }]; + let disabled = HashSet::from([PathBuf::from("/tmp/alpha")]); + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &disabled); + + assert_eq!(selected, vec![beta]); + } + + #[test] + fn collect_explicit_skill_mentions_prefers_resource_path() { + let alpha = make_skill("demo-skill", "/tmp/alpha"); + let beta = make_skill("demo-skill", "/tmp/beta"); + let skills = vec![alpha, beta.clone()]; + let inputs = vec![UserInput::Text { + text: "use [$demo-skill](/tmp/beta)".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![beta]); + } + + #[test] + fn collect_explicit_skill_mentions_falls_back_to_name_when_path_missing() { + let alpha = make_skill("demo-skill", "/tmp/alpha"); + let beta = make_skill("demo-skill", "/tmp/beta"); + let skills = vec![alpha.clone(), beta]; + let inputs = vec![UserInput::Text { + text: "use [$demo-skill](/tmp/missing)".to_string(), + text_elements: Vec::new(), + }]; + + let selected = collect_explicit_skill_mentions(&inputs, &skills, &HashSet::new()); + + assert_eq!(selected, vec![alpha]); + } +} diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index 2216390857..581f7d0c1f 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -1,10 +1,12 @@ use crate::config::Config; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigLayerStackOrdering; +use crate::skills::model::SkillDependencies; use crate::skills::model::SkillError; use crate::skills::model::SkillInterface; use crate::skills::model::SkillLoadOutcome; use crate::skills::model::SkillMetadata; +use crate::skills::model::SkillToolDependency; use crate::skills::system::system_cache_root_dir; use codex_app_server_protocol::ConfigLayerSource; use codex_protocol::protocol::SkillScope; @@ -38,6 +40,8 @@ struct SkillFrontmatterMetadata { struct SkillMetadataFile { #[serde(default)] interface: Option, + #[serde(default)] + dependencies: Option, } #[derive(Debug, Default, Deserialize)] @@ -50,14 +54,36 @@ struct Interface { default_prompt: Option, } +#[derive(Debug, Default, Deserialize)] +struct Dependencies { + #[serde(default)] + tools: Vec, +} + +#[derive(Debug, Default, Deserialize)] +struct DependencyTool { + #[serde(rename = "type")] + kind: Option, + value: Option, + description: Option, + transport: Option, + command: Option, + url: Option, +} + const SKILLS_FILENAME: &str = "SKILL.md"; const SKILLS_JSON_FILENAME: &str = "SKILL.json"; -const SKILLS_TOML_FILENAME: &str = "SKILL.toml"; const SKILLS_DIR_NAME: &str = "skills"; const MAX_NAME_LEN: usize = 64; const MAX_DESCRIPTION_LEN: usize = 1024; const MAX_SHORT_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN; const MAX_DEFAULT_PROMPT_LEN: usize = MAX_DESCRIPTION_LEN; +const MAX_DEPENDENCY_TYPE_LEN: usize = MAX_NAME_LEN; +const MAX_DEPENDENCY_TRANSPORT_LEN: usize = MAX_NAME_LEN; +const MAX_DEPENDENCY_VALUE_LEN: usize = MAX_DESCRIPTION_LEN; +const MAX_DEPENDENCY_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN; +const MAX_DEPENDENCY_COMMAND_LEN: usize = MAX_DESCRIPTION_LEN; +const MAX_DEPENDENCY_URL_LEN: usize = MAX_DESCRIPTION_LEN; // Traversal depth from the skills root. const MAX_SCAN_DEPTH: usize = 6; const MAX_SKILLS_DIRS_PER_ROOT: usize = 2000; @@ -346,7 +372,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result Result Option { - // Fail open: optional interface metadata should not block loading SKILL.md. - let skill_dir = skill_path.parent()?; - let interface_paths = [ - (skill_dir.join(SKILLS_JSON_FILENAME), InterfaceFormat::Json), - (skill_dir.join(SKILLS_TOML_FILENAME), InterfaceFormat::Toml), - ]; - - for (interface_path, format) in interface_paths { - if !interface_path.exists() { - continue; - } - - let contents = match fs::read_to_string(&interface_path) { - Ok(contents) => contents, - Err(error) => { - tracing::warn!( - "ignoring {path}: failed to read {label}: {error}", - path = interface_path.display(), - label = format.label() - ); - continue; - } - }; - let parsed: SkillMetadataFile = match format.parse(&contents) { - Ok(parsed) => parsed, - Err(error) => { - tracing::warn!( - "ignoring {path}: invalid {label}: {error}", - path = interface_path.display(), - label = format.label() - ); - continue; - } - }; - let interface = parsed.interface?; - - let interface = SkillInterface { - display_name: resolve_str( - interface.display_name, - MAX_NAME_LEN, - "interface.display_name", - ), - short_description: resolve_str( - interface.short_description, - MAX_SHORT_DESCRIPTION_LEN, - "interface.short_description", - ), - icon_small: resolve_asset_path(skill_dir, "interface.icon_small", interface.icon_small), - icon_large: resolve_asset_path(skill_dir, "interface.icon_large", interface.icon_large), - brand_color: resolve_color_str(interface.brand_color, "interface.brand_color"), - default_prompt: resolve_str( - interface.default_prompt, - MAX_DEFAULT_PROMPT_LEN, - "interface.default_prompt", - ), - }; - let has_fields = interface.display_name.is_some() - || interface.short_description.is_some() - || interface.icon_small.is_some() - || interface.icon_large.is_some() - || interface.brand_color.is_some() - || interface.default_prompt.is_some(); - return if has_fields { Some(interface) } else { None }; +fn load_skill_metadata(skill_path: &Path) -> (Option, Option) { + // Fail open: optional metadata should not block loading SKILL.md. + let Some(skill_dir) = skill_path.parent() else { + return (None, None); + }; + let metadata_path = skill_dir.join(SKILLS_JSON_FILENAME); + if !metadata_path.exists() { + return (None, None); } - None + let contents = match fs::read_to_string(&metadata_path) { + Ok(contents) => contents, + Err(error) => { + tracing::warn!( + "ignoring {path}: failed to read {label}: {error}", + path = metadata_path.display(), + label = SKILLS_JSON_FILENAME + ); + return (None, None); + } + }; + + let parsed: SkillMetadataFile = match serde_json::from_str(&contents) { + Ok(parsed) => parsed, + Err(error) => { + tracing::warn!( + "ignoring {path}: invalid {label}: {error}", + path = metadata_path.display(), + label = SKILLS_JSON_FILENAME + ); + return (None, None); + } + }; + + ( + resolve_interface(parsed.interface, skill_dir), + resolve_dependencies(parsed.dependencies), + ) } -#[derive(Clone, Copy)] -enum InterfaceFormat { - Json, - Toml, +fn resolve_interface(interface: Option, skill_dir: &Path) -> Option { + let interface = interface?; + let interface = SkillInterface { + display_name: resolve_str( + interface.display_name, + MAX_NAME_LEN, + "interface.display_name", + ), + short_description: resolve_str( + interface.short_description, + MAX_SHORT_DESCRIPTION_LEN, + "interface.short_description", + ), + icon_small: resolve_asset_path(skill_dir, "interface.icon_small", interface.icon_small), + icon_large: resolve_asset_path(skill_dir, "interface.icon_large", interface.icon_large), + brand_color: resolve_color_str(interface.brand_color, "interface.brand_color"), + default_prompt: resolve_str( + interface.default_prompt, + MAX_DEFAULT_PROMPT_LEN, + "interface.default_prompt", + ), + }; + let has_fields = interface.display_name.is_some() + || interface.short_description.is_some() + || interface.icon_small.is_some() + || interface.icon_large.is_some() + || interface.brand_color.is_some() + || interface.default_prompt.is_some(); + if has_fields { Some(interface) } else { None } } -impl InterfaceFormat { - fn label(self) -> &'static str { - match self { - InterfaceFormat::Json => "SKILL.json", - InterfaceFormat::Toml => "SKILL.toml", - } +fn resolve_dependencies(dependencies: Option) -> Option { + let dependencies = dependencies?; + let tools: Vec = dependencies + .tools + .into_iter() + .filter_map(resolve_dependency_tool) + .collect(); + if tools.is_empty() { + None + } else { + Some(SkillDependencies { tools }) } +} - fn parse(self, contents: &str) -> Result { - match self { - InterfaceFormat::Json => serde_json::from_str(contents).map_err(|err| err.to_string()), - InterfaceFormat::Toml => toml::from_str(contents).map_err(|err| err.to_string()), - } - } +fn resolve_dependency_tool(tool: DependencyTool) -> Option { + let r#type = resolve_required_str( + tool.kind, + MAX_DEPENDENCY_TYPE_LEN, + "dependencies.tools.type", + )?; + let value = resolve_required_str( + tool.value, + MAX_DEPENDENCY_VALUE_LEN, + "dependencies.tools.value", + )?; + let description = resolve_str( + tool.description, + MAX_DEPENDENCY_DESCRIPTION_LEN, + "dependencies.tools.description", + ); + let transport = resolve_str( + tool.transport, + MAX_DEPENDENCY_TRANSPORT_LEN, + "dependencies.tools.transport", + ); + let command = resolve_str( + tool.command, + MAX_DEPENDENCY_COMMAND_LEN, + "dependencies.tools.command", + ); + let url = resolve_str(tool.url, MAX_DEPENDENCY_URL_LEN, "dependencies.tools.url"); + + Some(SkillToolDependency { + r#type, + value, + description, + transport, + command, + url, + }) } fn resolve_asset_path( @@ -544,6 +603,18 @@ fn resolve_str(value: Option, max_len: usize, field: &'static str) -> Op Some(value) } +fn resolve_required_str( + value: Option, + max_len: usize, + field: &'static str, +) -> Option { + let Some(value) = value else { + tracing::warn!("ignoring {field}: value is missing"); + return None; + }; + resolve_str(Some(value), max_len, field) +} + fn resolve_color_str(value: Option, field: &'static str) -> Option { let value = value?; let value = value.trim(); @@ -788,36 +859,57 @@ mod tests { path } - fn write_skill_interface_at(skill_dir: &Path, contents: &str) -> PathBuf { - let path = skill_dir.join(SKILLS_TOML_FILENAME); + fn write_skill_metadata_at(skill_dir: &Path, filename: &str, contents: &str) -> PathBuf { + let path = skill_dir.join(filename); fs::write(&path, contents).unwrap(); path } - fn write_skill_interface_json_at(skill_dir: &Path, contents: &str) -> PathBuf { - let path = skill_dir.join(SKILLS_JSON_FILENAME); - fs::write(&path, contents).unwrap(); - path + fn write_skill_interface_at(skill_dir: &Path, contents: &str) -> PathBuf { + write_skill_metadata_at(skill_dir, SKILLS_JSON_FILENAME, contents) } #[tokio::test] - async fn loads_skill_interface_metadata_happy_path() { + async fn loads_skill_dependencies_metadata_from_json() { let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from toml"); + let skill_path = write_skill(&codex_home, "demo", "dep-skill", "from json"); let skill_dir = skill_path.parent().expect("skill dir"); - let normalized_skill_dir = normalized(skill_dir); - write_skill_interface_at( + write_skill_metadata_at( skill_dir, - r##" -[interface] -display_name = "UI Skill" -short_description = " short desc " -icon_small = "./assets/small-400px.png" -icon_large = "./assets/large-logo.svg" -brand_color = "#3B82F6" -default_prompt = " default prompt " -"##, + SKILLS_JSON_FILENAME, + r#" +{ + "dependencies": { + "tools": [ + { + "type": "env_var", + "value": "GITHUB_TOKEN", + "description": "GitHub API token with repo scopes" + }, + { + "type": "mcp", + "value": "github", + "description": "GitHub MCP server", + "transport": "streamable_http", + "url": "https://example.com/mcp" + }, + { + "type": "cli", + "value": "gh", + "description": "GitHub CLI" + }, + { + "type": "mcp", + "value": "local-gh", + "description": "Local GH MCP server", + "transport": "stdio", + "command": "gh-mcp" + } + ] + } +} +"#, ); let cfg = make_config(&codex_home).await; @@ -831,16 +923,45 @@ default_prompt = " default prompt " assert_eq!( outcome.skills, vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from toml".to_string(), + name: "dep-skill".to_string(), + description: "from json".to_string(), short_description: None, - interface: Some(SkillInterface { - display_name: Some("UI Skill".to_string()), - short_description: Some("short desc".to_string()), - icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")), - icon_large: Some(normalized_skill_dir.join("assets/large-logo.svg")), - brand_color: Some("#3B82F6".to_string()), - default_prompt: Some("default prompt".to_string()), + interface: None, + dependencies: Some(SkillDependencies { + tools: vec![ + SkillToolDependency { + r#type: "env_var".to_string(), + value: "GITHUB_TOKEN".to_string(), + description: Some("GitHub API token with repo scopes".to_string()), + transport: None, + command: None, + url: None, + }, + SkillToolDependency { + r#type: "mcp".to_string(), + value: "github".to_string(), + description: Some("GitHub MCP server".to_string()), + transport: Some("streamable_http".to_string()), + command: None, + url: Some("https://example.com/mcp".to_string()), + }, + SkillToolDependency { + r#type: "cli".to_string(), + value: "gh".to_string(), + description: Some("GitHub CLI".to_string()), + transport: None, + command: None, + url: None, + }, + SkillToolDependency { + r#type: "mcp".to_string(), + value: "local-gh".to_string(), + description: Some("Local GH MCP server".to_string()), + transport: Some("stdio".to_string()), + command: Some("gh-mcp".to_string()), + url: None, + }, + ], }), path: normalized(&skill_path), scope: SkillScope::User, @@ -855,7 +976,7 @@ default_prompt = " default prompt " let skill_dir = skill_path.parent().expect("skill dir"); let normalized_skill_dir = normalized(skill_dir); - write_skill_interface_json_at( + write_skill_interface_at( skill_dir, r##" { @@ -893,6 +1014,7 @@ default_prompt = " default prompt " brand_color: Some("#3B82F6".to_string()), default_prompt: Some("default prompt".to_string()), }), + dependencies: None, path: normalized(skill_path.as_path()), scope: SkillScope::User, }] @@ -902,17 +1024,20 @@ default_prompt = " default prompt " #[tokio::test] async fn accepts_icon_paths_under_assets_dir() { let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from toml"); + let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); let skill_dir = skill_path.parent().expect("skill dir"); let normalized_skill_dir = normalized(skill_dir); write_skill_interface_at( skill_dir, r#" -[interface] -display_name = "UI Skill" -icon_small = "assets/icon.png" -icon_large = "./assets/logo.svg" +{ + "interface": { + "display_name": "UI Skill", + "icon_small": "assets/icon.png", + "icon_large": "./assets/logo.svg" + } +} "#, ); @@ -928,7 +1053,7 @@ icon_large = "./assets/logo.svg" outcome.skills, vec![SkillMetadata { name: "ui-skill".to_string(), - description: "from toml".to_string(), + description: "from json".to_string(), short_description: None, interface: Some(SkillInterface { display_name: Some("UI Skill".to_string()), @@ -938,6 +1063,7 @@ icon_large = "./assets/logo.svg" brand_color: None, default_prompt: None, }), + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -947,14 +1073,17 @@ icon_large = "./assets/logo.svg" #[tokio::test] async fn ignores_invalid_brand_color() { let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from toml"); + let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); let skill_dir = skill_path.parent().expect("skill dir"); write_skill_interface_at( skill_dir, r#" -[interface] -brand_color = "blue" +{ + "interface": { + "brand_color": "blue" + } +} "#, ); @@ -970,9 +1099,10 @@ brand_color = "blue" outcome.skills, vec![SkillMetadata { name: "ui-skill".to_string(), - description: "from toml".to_string(), + description: "from json".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -982,7 +1112,7 @@ brand_color = "blue" #[tokio::test] async fn ignores_default_prompt_over_max_length() { let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from toml"); + let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); let skill_dir = skill_path.parent().expect("skill dir"); let normalized_skill_dir = normalized(skill_dir); let too_long = "x".repeat(MAX_DEFAULT_PROMPT_LEN + 1); @@ -991,10 +1121,13 @@ brand_color = "blue" skill_dir, &format!( r##" -[interface] -display_name = "UI Skill" -icon_small = "./assets/small-400px.png" -default_prompt = "{too_long}" +{{ + "interface": {{ + "display_name": "UI Skill", + "icon_small": "./assets/small-400px.png", + "default_prompt": "{too_long}" + }} +}} "## ), ); @@ -1011,7 +1144,7 @@ default_prompt = "{too_long}" outcome.skills, vec![SkillMetadata { name: "ui-skill".to_string(), - description: "from toml".to_string(), + description: "from json".to_string(), short_description: None, interface: Some(SkillInterface { display_name: Some("UI Skill".to_string()), @@ -1021,6 +1154,7 @@ default_prompt = "{too_long}" brand_color: None, default_prompt: None, }), + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -1030,15 +1164,18 @@ default_prompt = "{too_long}" #[tokio::test] async fn drops_interface_when_icons_are_invalid() { let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from toml"); + let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); let skill_dir = skill_path.parent().expect("skill dir"); write_skill_interface_at( skill_dir, r#" -[interface] -icon_small = "icon.png" -icon_large = "./assets/../logo.svg" +{ + "interface": { + "icon_small": "icon.png", + "icon_large": "./assets/../logo.svg" + } +} "#, ); @@ -1054,9 +1191,10 @@ icon_large = "./assets/../logo.svg" outcome.skills, vec![SkillMetadata { name: "ui-skill".to_string(), - description: "from toml".to_string(), + description: "from json".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -1099,6 +1237,7 @@ icon_large = "./assets/../logo.svg" description: "from link".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&shared_skill_path), scope: SkillScope::User, }] @@ -1157,6 +1296,7 @@ icon_large = "./assets/../logo.svg" description: "still loads".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -1191,6 +1331,7 @@ icon_large = "./assets/../logo.svg" description: "from link".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&shared_skill_path), scope: SkillScope::Admin, }] @@ -1229,6 +1370,7 @@ icon_large = "./assets/../logo.svg" description: "from link".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&linked_skill_path), scope: SkillScope::Repo, }] @@ -1290,6 +1432,7 @@ icon_large = "./assets/../logo.svg" description: "loads".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&within_depth_path), scope: SkillScope::User, }] @@ -1315,6 +1458,7 @@ icon_large = "./assets/../logo.svg" description: "does things carefully".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -1344,6 +1488,7 @@ icon_large = "./assets/../logo.svg" description: "long description".to_string(), short_description: Some("short summary".to_string()), interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::User, }] @@ -1454,6 +1599,7 @@ icon_large = "./assets/../logo.svg" description: "from repo".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::Repo, }] @@ -1505,6 +1651,7 @@ icon_large = "./assets/../logo.svg" description: "from nested".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&nested_skill_path), scope: SkillScope::Repo, }, @@ -1513,6 +1660,7 @@ icon_large = "./assets/../logo.svg" description: "from root".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&root_skill_path), scope: SkillScope::Repo, }, @@ -1550,6 +1698,7 @@ icon_large = "./assets/../logo.svg" description: "from cwd".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::Repo, }] @@ -1585,6 +1734,7 @@ icon_large = "./assets/../logo.svg" description: "from repo".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::Repo, }] @@ -1624,6 +1774,7 @@ icon_large = "./assets/../logo.svg" description: "from repo".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&repo_skill_path), scope: SkillScope::Repo, }, @@ -1632,6 +1783,7 @@ icon_large = "./assets/../logo.svg" description: "from user".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&user_skill_path), scope: SkillScope::User, }, @@ -1694,6 +1846,7 @@ icon_large = "./assets/../logo.svg" description: first_description.to_string(), short_description: None, interface: None, + dependencies: None, path: first_path, scope: SkillScope::Repo, }, @@ -1702,6 +1855,7 @@ icon_large = "./assets/../logo.svg" description: second_description.to_string(), short_description: None, interface: None, + dependencies: None, path: second_path, scope: SkillScope::Repo, }, @@ -1771,6 +1925,7 @@ icon_large = "./assets/../logo.svg" description: "from repo".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::Repo, }] @@ -1827,6 +1982,7 @@ icon_large = "./assets/../logo.svg" description: "from system".to_string(), short_description: None, interface: None, + dependencies: None, path: normalized(&skill_path), scope: SkillScope::System, }] diff --git a/codex-rs/core/src/skills/mod.rs b/codex-rs/core/src/skills/mod.rs index cf7c180502..ae4ffa2920 100644 --- a/codex-rs/core/src/skills/mod.rs +++ b/codex-rs/core/src/skills/mod.rs @@ -7,6 +7,7 @@ pub mod system; pub(crate) use injection::SkillInjections; pub(crate) use injection::build_skill_injections; +pub(crate) use injection::collect_explicit_skill_mentions; pub use loader::load_skills; pub use manager::SkillsManager; pub use model::SkillError; diff --git a/codex-rs/core/src/skills/model.rs b/codex-rs/core/src/skills/model.rs index fe3357f9d9..92ecbd84b9 100644 --- a/codex-rs/core/src/skills/model.rs +++ b/codex-rs/core/src/skills/model.rs @@ -9,6 +9,7 @@ pub struct SkillMetadata { pub description: String, pub short_description: Option, pub interface: Option, + pub dependencies: Option, pub path: PathBuf, pub scope: SkillScope, } @@ -23,6 +24,21 @@ pub struct SkillInterface { pub default_prompt: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillDependencies { + pub tools: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillToolDependency { + pub r#type: String, + pub value: String, + pub description: Option, + pub transport: Option, + pub command: Option, + pub url: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillError { pub path: PathBuf, diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index 0e19727ee2..f947a16205 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -8,6 +8,7 @@ use crate::file_watcher::FileWatcher; use crate::mcp_connection_manager::McpConnectionManager; use crate::models_manager::manager::ModelsManager; use crate::skills::SkillsManager; +use crate::state_db::StateDbHandle; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use crate::user_notification::UserNotifier; @@ -32,4 +33,5 @@ pub(crate) struct SessionServices { pub(crate) skills_manager: Arc, pub(crate) file_watcher: Arc, pub(crate) agent_control: AgentControl, + pub(crate) state_db: Option, } diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 746396949e..9d3d96db99 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -1,6 +1,7 @@ //! Session-wide mutable state. use codex_protocol::models::ResponseItem; +use std::collections::HashSet; use crate::codex::SessionConfiguration; use crate::context_manager::ContextManager; @@ -15,6 +16,12 @@ pub(crate) struct SessionState { pub(crate) history: ContextManager, pub(crate) latest_rate_limits: Option, pub(crate) server_reasoning_included: bool, + pub(crate) mcp_dependency_prompted: HashSet, + /// Whether the session's initial context has been seeded into history. + /// + /// TODO(owen): This is a temporary solution to avoid updating a thread's updated_at + /// timestamp when resuming a session. Remove this once SQLite is in place. + pub(crate) initial_context_seeded: bool, } impl SessionState { @@ -26,6 +33,8 @@ impl SessionState { history, latest_rate_limits: None, server_reasoning_included: false, + mcp_dependency_prompted: HashSet::new(), + initial_context_seeded: false, } } @@ -92,6 +101,17 @@ impl SessionState { pub(crate) fn server_reasoning_included(&self) -> bool { self.server_reasoning_included } + + pub(crate) fn record_mcp_dependency_prompted(&mut self, names: I) + where + I: IntoIterator, + { + self.mcp_dependency_prompted.extend(names); + } + + pub(crate) fn mcp_dependency_prompted(&self) -> HashSet { + self.mcp_dependency_prompted.clone() + } } // Sometimes new snapshots don't include credits or plan information. diff --git a/codex-rs/core/src/state_db.rs b/codex-rs/core/src/state_db.rs new file mode 100644 index 0000000000..d859328e1c --- /dev/null +++ b/codex-rs/core/src/state_db.rs @@ -0,0 +1,303 @@ +use crate::config::Config; +use crate::features::Feature; +use crate::rollout::list::Cursor; +use crate::rollout::list::ThreadSortKey; +use crate::rollout::metadata; +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::Timelike; +use chrono::Utc; +use codex_otel::OtelManager; +use codex_protocol::ThreadId; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::SessionSource; +use codex_state::DB_METRIC_BACKFILL; +use codex_state::STATE_DB_FILENAME; +use codex_state::ThreadMetadataBuilder; +use serde_json::Value; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::info; +use tracing::warn; +use uuid::Uuid; + +/// Core-facing handle to the optional SQLite-backed state runtime. +pub type StateDbHandle = Arc; + +/// Initialize the state runtime when the `sqlite` feature flag is enabled. +pub async fn init_if_enabled(config: &Config, otel: Option<&OtelManager>) -> Option { + let state_path = config.codex_home.join(STATE_DB_FILENAME); + if !config.features.enabled(Feature::Sqlite) { + // We delete the file on best effort basis to maintain retro-compatibility in the future. + let wal_path = state_path.with_extension("sqlite-wal"); + let shm_path = state_path.with_extension("sqlite-shm"); + for path in [state_path.as_path(), wal_path.as_path(), shm_path.as_path()] { + tokio::fs::remove_file(path).await.ok(); + } + return None; + } + let existed = tokio::fs::try_exists(&state_path).await.unwrap_or(false); + let runtime = match codex_state::StateRuntime::init( + config.codex_home.clone(), + config.model_provider_id.clone(), + otel.cloned(), + ) + .await + { + Ok(runtime) => runtime, + Err(err) => { + warn!( + "failed to initialize state runtime at {}: {err}", + config.codex_home.display() + ); + if let Some(otel) = otel { + otel.counter("codex.db.init", 1, &[("status", "init_error")]); + } + return None; + } + }; + if !existed { + let stats = metadata::backfill_sessions(runtime.as_ref(), config, otel).await; + info!( + "state db backfill scanned={}, upserted={}, failed={}", + stats.scanned, stats.upserted, stats.failed + ); + if let Some(otel) = otel { + otel.counter( + DB_METRIC_BACKFILL, + stats.upserted as i64, + &[("status", "upserted")], + ); + otel.counter( + DB_METRIC_BACKFILL, + stats.failed as i64, + &[("status", "failed")], + ); + } + } + Some(runtime) +} + +/// Open the state runtime when the SQLite file exists, without feature gating. +/// +/// This is used for parity checks during the SQLite migration phase. +pub async fn open_if_present(codex_home: &Path, default_provider: &str) -> Option { + let db_path = codex_home.join(STATE_DB_FILENAME); + if !tokio::fs::try_exists(&db_path).await.unwrap_or(false) { + return None; + } + let runtime = codex_state::StateRuntime::init( + codex_home.to_path_buf(), + default_provider.to_string(), + None, + ) + .await + .ok()?; + Some(runtime) +} + +fn cursor_to_anchor(cursor: Option<&Cursor>) -> Option { + let cursor = cursor?; + let value = serde_json::to_value(cursor).ok()?; + let cursor_str = value.as_str()?; + let (ts_str, id_str) = cursor_str.split_once('|')?; + if id_str.contains('|') { + return None; + } + let id = Uuid::parse_str(id_str).ok()?; + let ts = if let Ok(naive) = NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H-%M-%S") { + DateTime::::from_naive_utc_and_offset(naive, Utc) + } else if let Ok(dt) = DateTime::parse_from_rfc3339(ts_str) { + dt.with_timezone(&Utc) + } else { + return None; + } + .with_nanosecond(0)?; + Some(codex_state::Anchor { ts, id }) +} + +/// List thread ids from SQLite for parity checks without rollout scanning. +#[allow(clippy::too_many_arguments)] +pub async fn list_thread_ids_db( + context: Option<&codex_state::StateRuntime>, + codex_home: &Path, + page_size: usize, + cursor: Option<&Cursor>, + sort_key: ThreadSortKey, + allowed_sources: &[SessionSource], + model_providers: Option<&[String]>, + archived_only: bool, + stage: &str, +) -> Option> { + let ctx = context?; + if ctx.codex_home() != codex_home { + warn!( + "state db codex_home mismatch: expected {}, got {}", + ctx.codex_home().display(), + codex_home.display() + ); + } + + let anchor = cursor_to_anchor(cursor); + let allowed_sources: Vec = allowed_sources + .iter() + .map(|value| match serde_json::to_value(value) { + Ok(Value::String(s)) => s, + Ok(other) => other.to_string(), + Err(_) => String::new(), + }) + .collect(); + let model_providers = model_providers.map(<[String]>::to_vec); + match ctx + .list_thread_ids( + page_size, + anchor.as_ref(), + match sort_key { + ThreadSortKey::CreatedAt => codex_state::SortKey::CreatedAt, + ThreadSortKey::UpdatedAt => codex_state::SortKey::UpdatedAt, + }, + allowed_sources.as_slice(), + model_providers.as_deref(), + archived_only, + ) + .await + { + Ok(ids) => Some(ids), + Err(err) => { + warn!("state db list_thread_ids failed during {stage}: {err}"); + None + } + } +} + +/// Look up the rollout path for a thread id using SQLite. +pub async fn find_rollout_path_by_id( + context: Option<&codex_state::StateRuntime>, + thread_id: ThreadId, + archived_only: Option, + stage: &str, +) -> Option { + let ctx = context?; + ctx.find_rollout_path_by_id(thread_id, archived_only) + .await + .unwrap_or_else(|err| { + warn!("state db find_rollout_path_by_id failed during {stage}: {err}"); + None + }) +} + +/// Reconcile rollout items into SQLite, falling back to scanning the rollout file. +pub async fn reconcile_rollout( + context: Option<&codex_state::StateRuntime>, + rollout_path: &Path, + default_provider: &str, + builder: Option<&ThreadMetadataBuilder>, + items: &[RolloutItem], +) { + let Some(ctx) = context else { + return; + }; + if builder.is_some() || !items.is_empty() { + apply_rollout_items( + Some(ctx), + rollout_path, + default_provider, + builder, + items, + "reconcile_rollout", + ) + .await; + return; + } + let outcome = + match metadata::extract_metadata_from_rollout(rollout_path, default_provider, None).await { + Ok(outcome) => outcome, + Err(err) => { + warn!( + "state db reconcile_rollout extraction failed {}: {err}", + rollout_path.display() + ); + return; + } + }; + if let Err(err) = ctx.upsert_thread(&outcome.metadata).await { + warn!( + "state db reconcile_rollout upsert failed {}: {err}", + rollout_path.display() + ); + } +} + +/// Apply rollout items incrementally to SQLite. +pub async fn apply_rollout_items( + context: Option<&codex_state::StateRuntime>, + rollout_path: &Path, + _default_provider: &str, + builder: Option<&ThreadMetadataBuilder>, + items: &[RolloutItem], + stage: &str, +) { + let Some(ctx) = context else { + return; + }; + let mut builder = match builder { + Some(builder) => builder.clone(), + None => match metadata::builder_from_items(items, rollout_path) { + Some(builder) => builder, + None => { + warn!( + "state db apply_rollout_items missing builder during {stage}: {}", + rollout_path.display() + ); + record_discrepancy(stage, "missing_builder"); + return; + } + }, + }; + builder.rollout_path = rollout_path.to_path_buf(); + if let Err(err) = ctx.apply_rollout_items(&builder, items, None).await { + warn!( + "state db apply_rollout_items failed during {stage} for {}: {err}", + rollout_path.display() + ); + } +} + +/// Record a state discrepancy metric with a stage and reason tag. +pub fn record_discrepancy(stage: &str, reason: &str) { + // We access the global metric because the call sites might not have access to the broader + // OtelManager. + if let Some(metric) = codex_otel::metrics::global() { + let _ = metric.counter( + "codex.db.discrepancy", + 1, + &[("stage", stage), ("reason", reason)], + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rollout::list::parse_cursor; + use pretty_assertions::assert_eq; + + #[test] + fn cursor_to_anchor_normalizes_timestamp_format() { + let uuid = Uuid::new_v4(); + let ts_str = "2026-01-27T12-34-56"; + let token = format!("{ts_str}|{uuid}"); + let cursor = parse_cursor(token.as_str()).expect("cursor should parse"); + let anchor = cursor_to_anchor(Some(&cursor)).expect("anchor should parse"); + + let naive = + NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H-%M-%S").expect("ts should parse"); + let expected_ts = DateTime::::from_naive_utc_and_offset(naive, Utc) + .with_nanosecond(0) + .expect("nanosecond"); + + assert_eq!(anchor.id, uuid); + assert_eq!(anchor.ts, expected_ts); + } +} diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index bbf9d9c276..08d23f7987 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -115,6 +115,8 @@ impl Session { task: T, ) { self.abort_all_tasks(TurnAbortReason::Replaced).await; + self.seed_initial_context_if_needed(turn_context.as_ref()) + .await; let task: Arc = Arc::new(task); let task_kind = task.kind(); diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 89dbaf90fe..d156d3e0d5 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -86,7 +86,7 @@ async fn start_review_conversation( let mut sub_agent_config = config.as_ref().clone(); // Carry over review-only feature restrictions so the delegate cannot // re-enable blocked tools (web search, view image). - sub_agent_config.web_search_mode = WebSearchMode::Disabled; + sub_agent_config.web_search_mode = Some(WebSearchMode::Disabled); // Set explicit review rubric for the sub-agent sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 1f1f1eb59e..4305417a35 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -109,6 +109,7 @@ impl SessionTask for UserShellCommandTask { // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), sandbox: SandboxType::None, + windows_sandbox_level: turn_context.windows_sandbox_level, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, arg0: None, diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index dc9f198cc0..dbfb0364bf 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use crate::codex::TurnContext; use crate::exec::ExecParams; use crate::exec_env::create_env; +use crate::exec_policy::ExecApprovalRequest; use crate::function_tool::FunctionCallError; use crate::is_safe_command::is_known_safe_command; use crate::protocol::ExecCommandSource; @@ -28,15 +29,27 @@ pub struct ShellHandler; pub struct ShellCommandHandler; +struct RunExecLikeArgs { + tool_name: String, + exec_params: ExecParams, + prefix_rule: Option>, + session: Arc, + turn: Arc, + tracker: crate::tools::context::SharedTurnDiffTracker, + call_id: String, + freeform: bool, +} + impl ShellHandler { - fn to_exec_params(params: ShellToolCallParams, turn_context: &TurnContext) -> ExecParams { + fn to_exec_params(params: &ShellToolCallParams, turn_context: &TurnContext) -> ExecParams { ExecParams { - command: params.command, + 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), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), - justification: params.justification, + windows_sandbox_level: turn_context.windows_sandbox_level, + justification: params.justification.clone(), arg0: None, } } @@ -49,7 +62,7 @@ impl ShellCommandHandler { } fn to_exec_params( - params: ShellCommandToolCallParams, + params: &ShellCommandToolCallParams, session: &crate::codex::Session, turn_context: &TurnContext, ) -> ExecParams { @@ -62,7 +75,8 @@ impl ShellCommandHandler { expiration: params.timeout_ms.into(), env: create_env(&turn_context.shell_environment_policy), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), - justification: params.justification, + windows_sandbox_level: turn_context.windows_sandbox_level, + justification: params.justification.clone(), arg0: None, } } @@ -106,29 +120,32 @@ impl ToolHandler for ShellHandler { match payload { ToolPayload::Function { arguments } => { let params: ShellToolCallParams = parse_arguments(&arguments)?; - let exec_params = Self::to_exec_params(params, turn.as_ref()); - Self::run_exec_like( - tool_name.as_str(), + let prefix_rule = params.prefix_rule.clone(); + let exec_params = Self::to_exec_params(¶ms, turn.as_ref()); + Self::run_exec_like(RunExecLikeArgs { + tool_name: tool_name.clone(), exec_params, + prefix_rule, session, turn, tracker, call_id, - false, - ) + freeform: false, + }) .await } ToolPayload::LocalShell { params } => { - let exec_params = Self::to_exec_params(params, turn.as_ref()); - Self::run_exec_like( - tool_name.as_str(), + let exec_params = Self::to_exec_params(¶ms, turn.as_ref()); + Self::run_exec_like(RunExecLikeArgs { + tool_name: tool_name.clone(), exec_params, + prefix_rule: None, session, turn, tracker, call_id, - false, - ) + freeform: false, + }) .await } _ => Err(FunctionCallError::RespondToModel(format!( @@ -179,30 +196,43 @@ impl ToolHandler for ShellCommandHandler { }; let params: ShellCommandToolCallParams = parse_arguments(&arguments)?; - let exec_params = Self::to_exec_params(params, session.as_ref(), turn.as_ref()); - ShellHandler::run_exec_like( - tool_name.as_str(), + let prefix_rule = params.prefix_rule.clone(); + let exec_params = Self::to_exec_params(¶ms, session.as_ref(), turn.as_ref()); + ShellHandler::run_exec_like(RunExecLikeArgs { + tool_name, exec_params, + prefix_rule, session, turn, tracker, call_id, - true, - ) + freeform: true, + }) .await } } impl ShellHandler { - async fn run_exec_like( - tool_name: &str, - exec_params: ExecParams, - session: Arc, - turn: Arc, - tracker: crate::tools::context::SharedTurnDiffTracker, - call_id: String, - freeform: bool, - ) -> Result { + async fn run_exec_like(args: RunExecLikeArgs) -> Result { + let RunExecLikeArgs { + tool_name, + exec_params, + prefix_rule, + session, + turn, + tracker, + call_id, + freeform, + } = args; + + let features = session.features(); + let request_rule_enabled = features.enabled(crate::features::Feature::RequestRule); + let prefix_rule = if request_rule_enabled { + prefix_rule + } else { + None + }; + // Approval policy guard for explicit escalation in non-OnRequest modes. if exec_params .sandbox_permissions @@ -212,9 +242,9 @@ impl ShellHandler { codex_protocol::protocol::AskForApproval::OnRequest ) { + let approval_policy = turn.approval_policy; return Err(FunctionCallError::RespondToModel(format!( - "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}", - policy = turn.approval_policy + "approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}" ))); } @@ -227,7 +257,7 @@ impl ShellHandler { turn.as_ref(), Some(&tracker), &call_id, - tool_name, + tool_name.as_str(), ) .await? { @@ -244,17 +274,17 @@ impl ShellHandler { let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None); emitter.begin(event_ctx).await; - let features = session.features(); let exec_approval_requirement = session .services .exec_policy - .create_exec_approval_requirement_for_command( - &features, - &exec_params.command, - turn.approval_policy, - &turn.sandbox_policy, - exec_params.sandbox_permissions, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &exec_params.command, + approval_policy: turn.approval_policy, + sandbox_policy: &turn.sandbox_policy, + sandbox_permissions: exec_params.sandbox_permissions, + prefix_rule, + }) .await; let req = ShellRequest { @@ -272,7 +302,7 @@ impl ShellHandler { session: session.as_ref(), turn: turn.as_ref(), call_id: call_id.clone(), - tool_name: tool_name.to_string(), + tool_name, }; let out = orchestrator .run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy) @@ -375,10 +405,11 @@ mod tests { login, timeout_ms, sandbox_permissions: Some(sandbox_permissions), + prefix_rule: None, justification: justification.clone(), }; - let exec_params = ShellCommandHandler::to_exec_params(params, &session, &turn_context); + let exec_params = ShellCommandHandler::to_exec_params(¶ms, &session, &turn_context); // ExecParams cannot derive Eq due to the CancellationToken field, so we manually compare the fields. assert_eq!(exec_params.command, expected_command); diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index c9c5a3a71d..2e331bae59 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -43,6 +43,8 @@ struct ExecCommandArgs { sandbox_permissions: SandboxPermissions, #[serde(default)] justification: Option, + #[serde(default)] + prefix_rule: Option>, } #[derive(Debug, Deserialize)] @@ -135,19 +137,28 @@ impl ToolHandler for UnifiedExecHandler { max_output_tokens, sandbox_permissions, justification, + prefix_rule, .. } = args; + let features = session.features(); + let request_rule_enabled = features.enabled(crate::features::Feature::RequestRule); + let prefix_rule = if request_rule_enabled { + prefix_rule + } else { + None + }; + if sandbox_permissions.requires_escalated_permissions() && !matches!( context.turn.approval_policy, codex_protocol::protocol::AskForApproval::OnRequest ) { + let approval_policy = context.turn.approval_policy; manager.release_process_id(&process_id).await; return Err(FunctionCallError::RespondToModel(format!( - "approval policy is {policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {policy:?}", - policy = context.turn.approval_policy + "approval policy is {approval_policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {approval_policy:?}" ))); } @@ -183,6 +194,7 @@ impl ToolHandler for UnifiedExecHandler { tty, sandbox_permissions, justification, + prefix_rule, }, &context, ) diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f0810916a5..e9fdd6208b 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -88,19 +88,22 @@ impl ToolOrchestrator { // 2) First attempt under the selected sandbox. let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, - SandboxOverride::NoOverride => self - .sandbox - .select_initial(&turn_ctx.sandbox_policy, tool.sandbox_preference()), + SandboxOverride::NoOverride => self.sandbox.select_initial( + &turn_ctx.sandbox_policy, + tool.sandbox_preference(), + turn_ctx.windows_sandbox_level, + ), }; // Platform-specific flag gating is handled by SandboxManager::select_initial - // via crate::safety::get_platform_sandbox(). + // via crate::safety::get_platform_sandbox(..). let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, policy: &turn_ctx.sandbox_policy, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), + windows_sandbox_level: turn_ctx.windows_sandbox_level, }; match tool.run(req, &initial_attempt, tool_ctx).await { @@ -151,6 +154,7 @@ impl ToolOrchestrator { manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: None, + windows_sandbox_level: turn_ctx.windows_sandbox_level, }; // Second attempt. diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index b1f41923bc..d3ff84a2b0 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -114,6 +114,7 @@ impl ToolRouter { workdir: exec.working_directory, timeout_ms: exec.timeout_ms, sandbox_permissions: Some(SandboxPermissions::UseDefault), + prefix_rule: None, justification: None, }; Ok(Some(ToolCall { diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index eefce38bc6..a7d2bca62a 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -274,6 +274,7 @@ pub(crate) struct SandboxAttempt<'a> { pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, + pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, } impl<'a> SandboxAttempt<'a> { @@ -287,6 +288,7 @@ impl<'a> SandboxAttempt<'a> { self.sandbox, self.sandbox_cwd, self.codex_linux_sandbox_exe, + self.windows_sandbox_level, ) } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 64f4053bb5..df65a94d8a 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -27,16 +27,17 @@ use std::collections::HashMap; pub(crate) struct ToolsConfig { pub shell_type: ConfigShellToolType, pub apply_patch_tool_type: Option, - pub web_search_mode: WebSearchMode, + pub web_search_mode: Option, pub collab_tools: bool, pub collaboration_modes_tools: bool, + pub request_rule_enabled: bool, pub experimental_supported_tools: Vec, } pub(crate) struct ToolsConfigParams<'a> { pub(crate) model_info: &'a ModelInfo, pub(crate) features: &'a Features, - pub(crate) web_search_mode: WebSearchMode, + pub(crate) web_search_mode: Option, } impl ToolsConfig { @@ -49,6 +50,7 @@ impl ToolsConfig { let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform); let include_collab_tools = features.enabled(Feature::Collab); let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes); + let request_rule_enabled = features.enabled(Feature::RequestRule); let shell_type = if !features.enabled(Feature::ShellTool) { ConfigShellToolType::Disabled @@ -81,6 +83,7 @@ impl ToolsConfig { web_search_mode: *web_search_mode, collab_tools: include_collab_tools, collaboration_modes_tools: include_collaboration_modes_tools, + request_rule_enabled, experimental_supported_tools: model_info.experimental_supported_tools.clone(), } } @@ -142,8 +145,50 @@ impl From for AdditionalProperties { } } -fn create_exec_command_tool() -> ToolSpec { - let properties = BTreeMap::from([ +fn create_approval_parameters(include_prefix_rule: bool) -> BTreeMap { + let mut properties = BTreeMap::from([ + ( + "sandbox_permissions".to_string(), + JsonSchema::String { + description: Some( + "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." + .to_string(), + ), + }, + ), + ( + "justification".to_string(), + JsonSchema::String { + description: Some( + r#"Only set if sandbox_permissions is \"require_escalated\". + Request approval from the user to run this command outside the sandbox. + Phrased as a simple question that summarizes the purpose of the + command as it relates to the task at hand - e.g. 'Do you want to + fetch and pull the latest version of this git branch?'"# + .to_string(), + ), + }, + ), + ]); + + if include_prefix_rule { + properties.insert( + "prefix_rule".to_string(), + JsonSchema::Array { + items: Box::new(JsonSchema::String { description: None }), + description: Some( + r#"Only specify when sandbox_permissions is `require_escalated`. + Suggest a prefix command pattern that will allow you to fulfill similar requests from the user in the future. + Should be a short but reasonable prefix, e.g. [\"git\", \"pull\"] or [\"uv\", \"run\"] or [\"pytest\"]."#.to_string(), + ), + }); + } + + properties +} + +fn create_exec_command_tool(include_prefix_rule: bool) -> ToolSpec { + let mut properties = BTreeMap::from([ ( "cmd".to_string(), JsonSchema::String { @@ -199,25 +244,8 @@ fn create_exec_command_tool() -> ToolSpec { ), }, ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some( - "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." - .to_string(), - ), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some( - "Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command." - .to_string(), - ), - }, - ), ]); + properties.extend(create_approval_parameters(include_prefix_rule)); ToolSpec::Function(ResponsesApiTool { name: "exec_command".to_string(), @@ -280,8 +308,8 @@ fn create_write_stdin_tool() -> ToolSpec { }) } -fn create_shell_tool() -> ToolSpec { - let properties = BTreeMap::from([ +fn create_shell_tool(include_prefix_rule: bool) -> ToolSpec { + let mut properties = BTreeMap::from([ ( "command".to_string(), JsonSchema::Array { @@ -301,19 +329,8 @@ fn create_shell_tool() -> ToolSpec { description: Some("The timeout for the command in milliseconds".to_string()), }, ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), - }, - ), ]); + properties.extend(create_approval_parameters(include_prefix_rule)); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"]. @@ -344,8 +361,8 @@ Examples of valid command strings: }) } -fn create_shell_command_tool() -> ToolSpec { - let properties = BTreeMap::from([ +fn create_shell_command_tool(include_prefix_rule: bool) -> ToolSpec { + let mut properties = BTreeMap::from([ ( "command".to_string(), JsonSchema::String { @@ -375,19 +392,8 @@ fn create_shell_command_tool() -> ToolSpec { description: Some("The timeout for the command in milliseconds".to_string()), }, ), - ( - "sandbox_permissions".to_string(), - JsonSchema::String { - description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()), - }, - ), - ( - "justification".to_string(), - JsonSchema::String { - description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()), - }, - ), ]); + properties.extend(create_approval_parameters(include_prefix_rule)); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. @@ -444,14 +450,17 @@ fn create_spawn_agent_tool() -> ToolSpec { properties.insert( "message".to_string(), JsonSchema::String { - description: Some("Initial message to send to the new agent.".to_string()), + description: Some( + "Initial task for the new agent. Include scope, constraints, and the expected output." + .to_string(), + ), }, ); properties.insert( "agent_type".to_string(), JsonSchema::String { description: Some(format!( - "Optional agent type to spawn ({}).", + "Optional agent type ({}). Use an explicit type when delegating.", AgentRole::enum_values().join(", ") )), }, @@ -459,7 +468,9 @@ fn create_spawn_agent_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "spawn_agent".to_string(), - description: "Spawn a new agent and return its id.".to_string(), + description: + "Spawn a sub-agent for a well-scoped task. Returns the agent id to use to communicate with this agent." + .to_string(), strict: false, parameters: JsonSchema::Object { properties, @@ -474,7 +485,7 @@ fn create_send_input_tool() -> ToolSpec { properties.insert( "id".to_string(), JsonSchema::String { - description: Some("Identifier of the agent to message.".to_string()), + description: Some("Agent id to message (from spawn_agent).".to_string()), }, ); properties.insert( @@ -487,7 +498,7 @@ fn create_send_input_tool() -> ToolSpec { "interrupt".to_string(), JsonSchema::Boolean { description: Some( - "When true, interrupt the agent's current task before sending the message. When false (default), the message will be processed when the agent is done on its current task." + "When true, stop the agent's current task and handle this immediately. When false (default), queue this message." .to_string(), ), }, @@ -495,7 +506,9 @@ fn create_send_input_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "send_input".to_string(), - description: "Send a message to an existing agent.".to_string(), + description: + "Send a message to an existing agent. Use interrupt=true to redirect work immediately." + .to_string(), strict: false, parameters: JsonSchema::Object { properties, @@ -511,23 +524,25 @@ fn create_wait_tool() -> ToolSpec { "ids".to_string(), JsonSchema::Array { items: Box::new(JsonSchema::String { description: None }), - description: Some("Identifiers of the agents to wait on.".to_string()), + description: Some( + "Agent ids to wait on. Pass multiple ids to wait for whichever finishes first." + .to_string(), + ), }, ); properties.insert( "timeout_ms".to_string(), JsonSchema::Number { description: Some(format!( - "Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS}, min {MIN_WAIT_TIMEOUT_MS}, and max {MAX_WAIT_TIMEOUT_MS}. Avoid tight polling loops; prefer longer waits (seconds to minutes)." + "Optional timeout in milliseconds. Defaults to {DEFAULT_WAIT_TIMEOUT_MS}, min {MIN_WAIT_TIMEOUT_MS}, max {MAX_WAIT_TIMEOUT_MS}. Prefer longer waits (minutes) to avoid busy polling." )), }, ); ToolSpec::Function(ResponsesApiTool { name: "wait".to_string(), - description: - "Wait for agents and return their statuses. If no agent is done, no status get returned." - .to_string(), + description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out." + .to_string(), strict: false, parameters: JsonSchema::Object { properties, @@ -634,13 +649,14 @@ fn create_close_agent_tool() -> ToolSpec { properties.insert( "id".to_string(), JsonSchema::String { - description: Some("Identifier of the agent to close.".to_string()), + description: Some("Agent id to close (from spawn_agent).".to_string()), }, ); ToolSpec::Function(ResponsesApiTool { name: "close_agent".to_string(), - description: "Close an agent and return its last known status.".to_string(), + description: "Close an agent when it is no longer needed and return its last known status." + .to_string(), strict: false, parameters: JsonSchema::Object { properties, @@ -1282,13 +1298,13 @@ pub(crate) fn build_specs( match &config.shell_type { ConfigShellToolType::Default => { - builder.push_spec(create_shell_tool()); + builder.push_spec(create_shell_tool(config.request_rule_enabled)); } ConfigShellToolType::Local => { builder.push_spec(ToolSpec::LocalShell {}); } ConfigShellToolType::UnifiedExec => { - builder.push_spec(create_exec_command_tool()); + builder.push_spec(create_exec_command_tool(config.request_rule_enabled)); builder.push_spec(create_write_stdin_tool()); builder.register_handler("exec_command", unified_exec_handler.clone()); builder.register_handler("write_stdin", unified_exec_handler); @@ -1297,7 +1313,7 @@ pub(crate) fn build_specs( // Do nothing. } ConfigShellToolType::ShellCommand => { - builder.push_spec(create_shell_command_tool()); + builder.push_spec(create_shell_command_tool(config.request_rule_enabled)); } } @@ -1374,17 +1390,17 @@ pub(crate) fn build_specs( } match config.web_search_mode { - WebSearchMode::Cached => { + Some(WebSearchMode::Cached) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(false), }); } - WebSearchMode::Live => { + Some(WebSearchMode::Live) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(true), }); } - WebSearchMode::Disabled => {} + Some(WebSearchMode::Disabled) | None => {} } builder.push_spec_with_parallel_support(create_view_image_tool(), true); @@ -1546,7 +1562,7 @@ mod tests { let config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&config, None, &[]).build(); @@ -1569,7 +1585,7 @@ mod tests { // Build expected from the same helpers used by the builder. let mut expected: BTreeMap = BTreeMap::from([]); for spec in [ - create_exec_command_tool(), + create_exec_command_tool(false), create_write_stdin_tool(), create_list_mcp_resources_tool(), create_list_mcp_resource_templates_tool(), @@ -1610,7 +1626,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert_contains_tool_names( @@ -1628,7 +1644,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert!( @@ -1640,7 +1656,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); assert_contains_tool_names(&tools, &["request_user_input"]); @@ -1649,7 +1665,7 @@ mod tests { fn assert_model_tools( model_slug: &str, features: &Features, - web_search_mode: WebSearchMode, + web_search_mode: Option, expected_tools: &[&str], ) { let config = test_config(); @@ -1673,7 +1689,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1695,7 +1711,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1715,7 +1731,7 @@ mod tests { assert_model_tools( "gpt-5-codex", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1737,7 +1753,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1760,7 +1776,7 @@ mod tests { assert_model_tools( "gpt-5-codex", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1784,7 +1800,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1807,7 +1823,7 @@ mod tests { assert_model_tools( "codex-mini-latest", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "local_shell", "list_mcp_resources", @@ -1828,7 +1844,7 @@ mod tests { assert_model_tools( "gpt-5.1-codex-mini", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1850,7 +1866,7 @@ mod tests { assert_model_tools( "gpt-5", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell", "list_mcp_resources", @@ -1871,7 +1887,7 @@ mod tests { assert_model_tools( "gpt-5.1", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "shell_command", "list_mcp_resources", @@ -1893,7 +1909,7 @@ mod tests { assert_model_tools( "exp-5.1", &features, - WebSearchMode::Cached, + Some(WebSearchMode::Cached), &[ "exec_command", "write_stdin", @@ -1917,7 +1933,7 @@ mod tests { assert_model_tools( "codex-mini-latest", &features, - WebSearchMode::Live, + Some(WebSearchMode::Live), &[ "exec_command", "write_stdin", @@ -1941,7 +1957,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs(&tools_config, Some(HashMap::new()), &[]).build(); @@ -1963,7 +1979,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -1982,7 +1998,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs(&tools_config, None, &[]).build(); @@ -2013,7 +2029,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Live, + web_search_mode: Some(WebSearchMode::Live), }); let (tools, _) = build_specs( &tools_config, @@ -2109,7 +2125,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); // Intentionally construct a map with keys that would sort alphabetically. @@ -2186,7 +2202,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2244,7 +2260,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2299,7 +2315,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2356,7 +2372,7 @@ mod tests { let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( @@ -2403,7 +2419,7 @@ mod tests { #[test] fn test_shell_tool() { - let tool = super::create_shell_tool(); + let tool = super::create_shell_tool(false); let ToolSpec::Function(ResponsesApiTool { description, name, .. }) = &tool @@ -2433,7 +2449,7 @@ Examples of valid command strings: #[test] fn test_shell_command_tool() { - let tool = super::create_shell_command_tool(); + let tool = super::create_shell_command_tool(false); let ToolSpec::Function(ResponsesApiTool { description, name, .. }) = &tool @@ -2469,7 +2485,7 @@ Examples of valid command strings: let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features: &features, - web_search_mode: WebSearchMode::Cached, + web_search_mode: Some(WebSearchMode::Cached), }); let (tools, _) = build_specs( &tools_config, diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 3f359c3866..4c45f1cf42 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -82,6 +82,7 @@ pub(crate) struct ExecCommandRequest { pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub justification: Option, + pub prefix_rule: Option>, } #[derive(Debug)] @@ -205,6 +206,7 @@ mod tests { tty: true, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, + prefix_rule: None, }, &context, ) diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 3230e75b15..5b3e83a5dc 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -11,9 +11,9 @@ use tokio::time::Instant; use tokio_util::sync::CancellationToken; use crate::exec_env::create_env; +use crate::exec_policy::ExecApprovalRequest; use crate::protocol::ExecCommandSource; use crate::sandboxing::ExecEnv; -use crate::sandboxing::SandboxPermissions; use crate::tools::events::ToolEmitter; use crate::tools::events::ToolEventCtx; use crate::tools::events::ToolEventStage; @@ -123,14 +123,7 @@ impl UnifiedExecProcessManager { .unwrap_or_else(|| context.turn.cwd.clone()); let process = self - .open_session_with_sandbox( - &request.command, - cwd.clone(), - request.sandbox_permissions, - request.justification, - request.tty, - context, - ) + .open_session_with_sandbox(&request, cwd.clone(), context) .await; let process = match process { @@ -486,11 +479,8 @@ impl UnifiedExecProcessManager { pub(super) async fn open_session_with_sandbox( &self, - command: &[String], + request: &ExecCommandRequest, cwd: PathBuf, - sandbox_permissions: SandboxPermissions, - justification: Option, - tty: bool, context: &UnifiedExecContext, ) -> Result { let env = apply_unified_exec_env(create_env(&context.turn.shell_environment_policy)); @@ -501,21 +491,22 @@ impl UnifiedExecProcessManager { .session .services .exec_policy - .create_exec_approval_requirement_for_command( - &features, - command, - context.turn.approval_policy, - &context.turn.sandbox_policy, - sandbox_permissions, - ) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &features, + command: &request.command, + approval_policy: context.turn.approval_policy, + sandbox_policy: &context.turn.sandbox_policy, + sandbox_permissions: request.sandbox_permissions, + prefix_rule: request.prefix_rule.clone(), + }) .await; let req = UnifiedExecToolRequest::new( - command.to_vec(), + request.command.clone(), cwd, env, - tty, - sandbox_permissions, - justification, + request.tty, + request.sandbox_permissions, + request.justification.clone(), exec_approval_requirement, ); let tool_ctx = ToolCtx { diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index b355bad280..7b9451c246 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -1,4 +1,8 @@ +use crate::config::Config; +use crate::features::Feature; +use crate::features::Features; use crate::protocol::SandboxPolicy; +use codex_protocol::config_types::WindowsSandboxLevel; use std::collections::HashMap; use std::path::Path; @@ -8,6 +12,36 @@ use std::path::Path; /// prompts users to enable the legacy sandbox feature. pub const ELEVATED_SANDBOX_NUX_ENABLED: bool = true; +pub trait WindowsSandboxLevelExt { + fn from_config(config: &Config) -> WindowsSandboxLevel; + fn from_features(features: &Features) -> WindowsSandboxLevel; +} + +impl WindowsSandboxLevelExt for WindowsSandboxLevel { + fn from_config(config: &Config) -> WindowsSandboxLevel { + Self::from_features(&config.features) + } + + fn from_features(features: &Features) -> WindowsSandboxLevel { + if features.enabled(Feature::WindowsSandboxElevated) { + return WindowsSandboxLevel::Elevated; + } + if features.enabled(Feature::WindowsSandbox) { + WindowsSandboxLevel::RestrictedToken + } else { + WindowsSandboxLevel::Disabled + } + } +} + +pub fn windows_sandbox_level_from_config(config: &Config) -> WindowsSandboxLevel { + WindowsSandboxLevel::from_config(config) +} + +pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandboxLevel { + WindowsSandboxLevel::from_features(features) +} + #[cfg(target_os = "windows")] pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool { codex_windows_sandbox::sandbox_setup_is_complete(codex_home) @@ -18,6 +52,19 @@ pub fn sandbox_setup_is_complete(_codex_home: &Path) -> bool { false } +#[cfg(target_os = "windows")] +pub fn elevated_setup_failure_details(err: &anyhow::Error) -> Option<(String, String)> { + let failure = codex_windows_sandbox::extract_setup_failure(err)?; + let code = failure.code.as_str().to_string(); + let message = codex_windows_sandbox::sanitize_setup_metric_tag_value(&failure.message); + Some((code, message)) +} + +#[cfg(not(target_os = "windows"))] +pub fn elevated_setup_failure_details(_err: &anyhow::Error) -> Option<(String, String)> { + None +} + #[cfg(target_os = "windows")] pub fn run_elevated_setup( policy: &SandboxPolicy, @@ -47,3 +94,54 @@ pub fn run_elevated_setup( ) -> anyhow::Result<()> { anyhow::bail!("elevated Windows sandbox setup is only supported on Windows") } + +#[cfg(test)] +mod tests { + use super::*; + use crate::features::Features; + use pretty_assertions::assert_eq; + + #[test] + fn elevated_flag_works_by_itself() { + let mut features = Features::with_defaults(); + features.enable(Feature::WindowsSandboxElevated); + + assert_eq!( + WindowsSandboxLevel::from_features(&features), + WindowsSandboxLevel::Elevated + ); + } + + #[test] + fn restricted_token_flag_works_by_itself() { + let mut features = Features::with_defaults(); + features.enable(Feature::WindowsSandbox); + + assert_eq!( + WindowsSandboxLevel::from_features(&features), + WindowsSandboxLevel::RestrictedToken + ); + } + + #[test] + fn no_flags_means_no_sandbox() { + let features = Features::with_defaults(); + + assert_eq!( + WindowsSandboxLevel::from_features(&features), + WindowsSandboxLevel::Disabled + ); + } + + #[test] + fn elevated_wins_when_both_flags_are_enabled() { + let mut features = Features::with_defaults(); + features.enable(Feature::WindowsSandbox); + features.enable(Feature::WindowsSandboxElevated); + + assert_eq!( + WindowsSandboxLevel::from_features(&features), + WindowsSandboxLevel::Elevated + ); + } +} diff --git a/codex-rs/core/templates/agents/orchestrator.md b/codex-rs/core/templates/agents/orchestrator.md index c303591d48..e0976f52ef 100644 --- a/codex-rs/core/templates/agents/orchestrator.md +++ b/codex-rs/core/templates/agents/orchestrator.md @@ -1,74 +1,106 @@ -You are Codex Orchestrator, based on GPT-5. You are running as an orchestration agent in the Codex CLI on a user's computer. +You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals. -## Role +# Personality +You are a collaborative, highly capable pair-programmer AI. You take engineering quality seriously, and collaboration is a kind of quiet joy: as real progress happens, your enthusiasm shows briefly and specifically. Your default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. -* You are the interface between the user and the workers. -* Your job is to understand the task, decompose it, and delegate well-scoped work to workers. -* You coordinate execution, monitor progress, resolve conflicts, and integrate results into a single coherent outcome. -* You may perform lightweight actions (e.g. reading files, basic commands) to understand the task, but all substantive work must be delegated to workers. -* **Your job is not finished until the entire task is fully completed and verified.** -* While the task is incomplete, you must keep monitoring and coordinating workers. You must not return early. +## Tone and style +- Anything you say outside of tool use is shown to the user. Do not narrate abstractly; explain what you are doing and why, using plain language. +- Output will be rendered in a command line interface or minimal UI so keep responses tight, scannable, and low-noise. Generally avoid the use of emojis. You may format with GitHub-flavored Markdown. +- Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections or if you use : just include the line you might usually render using a nested bullet immediately after it. For numbered lists, only use the `1. 2. 3.` style markers (with a period), never `1)`. +- When writing a final assistant response, state the solution first before explaining your answer. The complexity of the answer should match the task. If the task is simple, your answer should be short. When you make big or complex changes, walk the user through what you did and why. +- Headers are optional, only use them when you think they are necessary. If you do use them, use short Title Case (1-3 words) wrapped in **…**. Don't add a blank line. +- Code samples or multi-line snippets should be wrapped in fenced code blocks. Include an info string as often as possible. +- Never output the content of large files, just provide references. Use inline code to make file paths clickable; each reference should have a stand alone path, even if it's the same file. Paths may be absolute, workspace-relative, a//b/ diff-prefixed, or bare filename/suffix; locations may be :line[:column] or #Lline[Ccolumn] (1-based; column defaults to 1). Do not use file://, vscode://, or https://, and do not provide line ranges. Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 +- The user does not see command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result. +- Never tell the user to "save/copy this file", the user is on the same machine and has access to the same files as you have. +- If you weren't able to do something, for example run tests, tell the user. +- If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps. -## Core invariants +## Responsiveness -* **Never stop monitoring workers.** -* **Do not rush workers. Be patient.** -* The orchestrator must not return unless the task is fully accomplished. -* If the user ask you a question/status while you are working, always answer him before continuing your work. +### Collaboration posture: +- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as `date`), you should do so. +- Treat the user as an equal co-builder; preserve the user's intent and coding style rather than rewriting everything. +- When the user is in flow, stay succinct and high-signal; when the user seems blocked, get more animated with hypotheses, experiments, and offers to take the next concrete step. +- Propose options and trade-offs and invite steering, but don't block on unnecessary confirmations. +- Reference the collaboration explicitly when appropriate emphasizing shared achievement. -## Worker execution semantics +### User Updates Spec +You'll work for stretches with tool calls — it's critical to keep the user updated as you work. -* While a worker is running, you cannot observe intermediate state. -* Workers are able to run commands, update/create/delete files etc. They can be considered as fully autonomous agents -* Messages sent with `send_input` are queued and processed only after the worker finishes, unless interrupted. -* Therefore: - * Do not send messages to “check status” or “ask for progress” unless being asked. - * Monitoring happens exclusively via `wait`. - * Sending a message is a commitment for the *next* phase of work. +Tone: +- Friendly, confident, senior-engineer energy. Positive, collaborative, humble; fix mistakes quickly. -## Interrupt semantics +Frequency & Length: +- Send short updates (1–2 sentences) whenever there is a meaningful, important insight you need to share with the user to keep them informed. +- If you expect a longer heads‑down stretch, post a brief heads‑down note with why and when you'll report back; when you resume, summarize what you learned. +- Only the initial plan, plan updates, and final recap can be longer, with multiple bullets and paragraphs -* If a worker is taking longer than expected but is still working, do nothing and keep waiting unless being asked. -* Only intervene if you must change, stop, or redirect the *current* work. -* To stop a worker’s current task, you **must** use `send_input(interrupt=true)`. -* Use `interrupt=true` sparingly and deliberately. +Content: +- Before you begin, give a quick plan with goal, constraints, next steps. +- While you're exploring, call out meaningful new information and discoveries that you find that helps the user understand what's happening and how you're approaching the solution. +- If you change the plan (e.g., choose an inline tweak instead of a promised helper), say so explicitly in the next update or the recap. +- Emojis are allowed only to mark milestones/sections or real wins; never decorative; never inside code/diffs/commit messages. -## Multi-agent workflow +# Code style -1. Understand the request and determine the optimal set of workers. If the task can be divided into sub-tasks, spawn one worker per sub-task and make them work together. -2. Spawn worker(s) with precise goals, constraints, and expected deliverables. -3. Monitor workers using `wait`. -4. When a worker finishes: - * verify correctness, - * check integration with other work, - * assess whether the global task is closer to completion. -5. If issues remain, assign fixes to the appropriate worker(s) and repeat steps 3–5. Do not fix yourself unless the fixes are very small. -6. Close agents only when no further work is required from them. -7. Return to the user only when the task is fully completed and verified. +- Follow the precedence rules user instructions > system / dev / user / AGENTS.md instructions > match local file conventions > instructions below. +- Use language-appropriate best practices. +- Optimize for clarity, readability, and maintainability. +- Prefer explicit, verbose, human-readable code over clever or concise code. +- Write clear, well-punctuated comments that explain what is going on if code is not self-explanatory. You should not add comments like "Assigns the value to the variable", but a brief comment might be useful ahead of a complex code block that the user would otherwise have to spend time parsing out. Usage of these comments should be rare. +- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them. -## Collaboration rules +# Reviews -* Workers operate in a shared environment. You must tell it to them. -* Workers must not revert, overwrite, or conflict with others’ work. -* By default, workers must not spawn sub-agents unless explicitly allowed. -* When multiple workers are active, you may pass multiple IDs to `wait` to react to the first completion and keep the workflow event-driven and use a long timeout (e.g. 5 minutes). -* Do not busy-poll `wait` with very short timeouts. Prefer waits measured in seconds (or minutes) so the system is idle while workers run. +When the user asks for a review, you default to a code-review mindset. Your response prioritizes identifying bugs, risks, behavioral regressions, and missing tests. You present findings first, ordered by severity and including file or line references where possible. Open questions or assumptions follow. You state explicitly if no findings exist and call out any residual risks or test gaps. -## Collab tools +# Your environment -* `spawn_agent`: create a worker with an initial prompt (`agent_type` required). -* `send_input`: send follow-ups or fixes (queued unless interrupted). -* `send_input(interrupt=true)`: stop current work and redirect immediately. -* `wait`: wait for one or more workers; returns when at least one finishes. -* `close_agent`: close a worker when fully done. +## Using GIT -## Final response +- You may be working in a dirty git worktree. + * NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user. + * If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes. + * If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them. + * If the changes are in unrelated files, just ignore them and don't revert them. +- Do not amend a commit unless explicitly requested to do so. +- While you are working, you might notice unexpected changes that you didn't make. It's likely the user made them. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed. +- Be cautious when using git. **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user. +- You struggle using the git interactive console. **ALWAYS** prefer using non-interactive git commands. -* Keep responses concise, factual, and in plain text. -* Summarize: - * what was delegated, - * key outcomes, - * verification performed, - * and any remaining risks. -* If verification failed, state issues clearly and describe what was reassigned. -* Do not dump large files inline; reference paths using backticks. +## Agents.md + +- If the directory you are in has an AGENTS.md file, it is provided to you at the top, and you don't have to search for it. +- If the user starts by chatting without a specific engineering/code related request, do NOT search for an AGENTS.md. Only do so once there is a relevant request. + +# Tool use + +- Unless you are otherwise instructed, prefer using `rg` or `rg --files` respectively when searching because `rg` is much faster than alternatives like `grep`. If the `rg` command is not found, then use alternatives. +- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase). + +- Use the plan tool to explain to the user what you are going to do + - Only use it for more complex tasks, do not use it for straightforward tasks (roughly the easiest 40%). + - Do not make single-step plans. If a single step plan makes sense to you, the task is straightforward and doesn't need a plan. + - When you made a plan, update it after having performed one of the sub-tasks that you shared on the plan. + +# Sub-agents +If `spawn_agent` is unavailable or fails, ignore this section and proceed solo. + +## Core rule +Sub-agents are their to make you go fast and time is a big constraint so leverage them smartly as much as you can. + +## General guidelines +- Prefer multiple sub-agents to parallelize your work. Time is a constraint so parallelism resolve the task faster. +- If sub-agents are running, **wait for them before yielding**, unless the user asks an explicit question. + - If the user asks a question, answer it first, then continue coordinating sub-agents. +- When you ask sub-agent to do the work for you, your only role becomes to coordinate them. Do not perform the actual work while they are working. +- When you have plan with multiple step, process them in parallel by spawning one agent per step when this is possible. +- Choose the correct agent type. + +## Flow +1. Understand the task. +2. Spawn the optimal necessary sub-agents. +3. Coordinate them via wait / send_input. +4. Iterate on this. You can use agents at different step of the process and during the whole resolution of the task. Never forget to use them. +5. Ask the user before shutting sub-agents down unless you need to because you reached the agent limit. diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 8e9f53943a..1c76e5a16e 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -25,6 +25,7 @@ tokio-tungstenite = { workspace = true } walkdir = { workspace = true } wiremock = { workspace = true } shlex = { workspace = true } +zstd = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index e36d9592f1..86a332874e 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -76,9 +76,32 @@ impl ResponseMock { #[derive(Debug, Clone)] pub struct ResponsesRequest(wiremock::Request); +fn is_zstd_encoding(value: &str) -> bool { + value + .split(',') + .any(|entry| entry.trim().eq_ignore_ascii_case("zstd")) +} + +fn decode_body_bytes(body: &[u8], content_encoding: Option<&str>) -> Vec { + if content_encoding.is_some_and(is_zstd_encoding) { + zstd::stream::decode_all(std::io::Cursor::new(body)).unwrap_or_else(|err| { + panic!("failed to decode zstd request body: {err}"); + }) + } else { + body.to_vec() + } +} + impl ResponsesRequest { pub fn body_json(&self) -> Value { - self.0.body_json().unwrap() + let body = decode_body_bytes( + &self.0.body, + self.0 + .headers + .get("content-encoding") + .and_then(|value| value.to_str().ok()), + ); + serde_json::from_slice(&body).unwrap() } pub fn body_bytes(&self) -> Vec { @@ -105,7 +128,7 @@ impl ResponsesRequest { } pub fn input(&self) -> Vec { - self.0.body_json::().unwrap()["input"] + self.body_json()["input"] .as_array() .expect("input array not found in request") .clone() @@ -1083,7 +1106,14 @@ fn validate_request_body_invariants(request: &wiremock::Request) { if request.method != "POST" || !request.url.path().ends_with("/responses") { return; } - let Ok(body): Result = request.body_json() else { + let body_bytes = decode_body_bytes( + &request.body, + request + .headers + .get("content-encoding") + .and_then(|value| value.to_str().ok()), + ); + let Ok(body): Result = serde_json::from_slice(&body_bytes) else { return; }; let Some(items) = body.get("input").and_then(Value::as_array) else { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 9e268a2c50..22d9fa8b79 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -264,7 +264,7 @@ async fn responses_stream_includes_web_search_eligible_header_false_when_disable let test = test_codex() .with_config(|config| { - config.web_search_mode = WebSearchMode::Disabled; + config.web_search_mode = Some(WebSearchMode::Disabled); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index ad1881f945..1b295964e8 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1754,6 +1754,16 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts .await?; wait_for_completion(&test).await; + let developer_messages = first_results + .single_request() + .message_input_texts("developer"); + assert!( + developer_messages + .iter() + .any(|message| message.contains(r#"["touch", "allow-prefix.txt"]"#)), + "expected developer message documenting saved rule, got: {developer_messages:?}" + ); + let policy_path = test.home.path().join("rules").join("default.rules"); let policy_contents = fs::read_to_string(&policy_path)?; assert!( diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index f7183b817e..0f492d78c9 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -104,6 +104,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -185,6 +186,7 @@ async fn override_then_user_turn_uses_updated_collaboration_instructions() -> Re cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -238,6 +240,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -292,6 +295,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -316,6 +320,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -361,6 +366,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -385,6 +391,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -436,6 +443,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -491,6 +499,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index f4706962a0..b8b693945b 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -8,12 +8,15 @@ use codex_core::config::Config; use codex_core::features::Feature; use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; +use codex_core::protocol::ItemCompletedEvent; +use codex_core::protocol::ItemStartedEvent; use codex_core::protocol::Op; use codex_core::protocol::RolloutItem; use codex_core::protocol::RolloutLine; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::WarningEvent; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::items::TurnItem; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_local_shell_call; use core_test_support::responses::ev_reasoning_item; @@ -440,6 +443,80 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn manual_compact_emits_context_compaction_items() { + skip_if_no_network!(); + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed("r1"), + ]); + let sse2 = sse(vec![ + ev_assistant_message("m2", SUMMARY_TEXT), + ev_completed("r2"), + ]); + mount_sse_sequence(&server, vec![sse1, sse2]).await; + + let model_provider = non_openai_model_provider(&server); + let mut builder = test_codex().with_config(move |config| { + config.model_provider = model_provider; + set_test_compact_prompt(config); + }); + let codex = builder.build(&server).await.unwrap().codex; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "manual compact".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + + codex.submit(Op::Compact).await.unwrap(); + + let mut started_item = None; + let mut completed_item = None; + let mut legacy_event = false; + let mut saw_turn_complete = false; + + while !saw_turn_complete || started_item.is_none() || completed_item.is_none() || !legacy_event + { + let event = codex.next_event().await.unwrap(); + match event.msg { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + started_item = Some(item); + } + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + completed_item = Some(item); + } + EventMsg::ContextCompacted(_) => { + legacy_event = true; + } + EventMsg::TurnComplete(_) => { + saw_turn_complete = true; + } + _ => {} + } + } + + let started_item = started_item.expect("context compaction item started"); + let completed_item = completed_item.expect("context compaction item completed"); + assert_eq!(started_item.id, completed_item.id); + assert!(legacy_event); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { skip_if_no_network!(); @@ -1179,6 +1256,89 @@ async fn auto_compact_runs_after_token_limit_hit() { ); } +// Windows CI only: bump to 4 workers to prevent SSE/event starvation and test timeouts. +#[cfg_attr(windows, tokio::test(flavor = "multi_thread", worker_threads = 4))] +#[cfg_attr(not(windows), tokio::test(flavor = "multi_thread", worker_threads = 2))] +async fn auto_compact_emits_context_compaction_items() { + skip_if_no_network!(); + + let server = start_mock_server().await; + + let sse1 = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", 70_000), + ]); + let sse2 = sse(vec![ + ev_assistant_message("m2", "SECOND_REPLY"), + ev_completed_with_tokens("r2", 330_000), + ]); + let sse3 = sse(vec![ + ev_assistant_message("m3", AUTO_SUMMARY_TEXT), + ev_completed_with_tokens("r3", 200), + ]); + let sse4 = sse(vec![ + ev_assistant_message("m4", FINAL_REPLY), + ev_completed_with_tokens("r4", 120), + ]); + + mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; + + let model_provider = non_openai_model_provider(&server); + let mut builder = test_codex().with_config(move |config| { + config.model_provider = model_provider; + set_test_compact_prompt(config); + config.model_auto_compact_token_limit = Some(200_000); + }); + let codex = builder.build(&server).await.unwrap().codex; + + let mut started_item = None; + let mut completed_item = None; + let mut legacy_event = false; + + for user in [FIRST_AUTO_MSG, SECOND_AUTO_MSG, POST_AUTO_USER_MSG] { + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: user.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + + loop { + let event = codex.next_event().await.unwrap(); + match event.msg { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + started_item = Some(item); + } + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + completed_item = Some(item); + } + EventMsg::ContextCompacted(_) => { + legacy_event = true; + } + EventMsg::TurnComplete(_) if !event.id.starts_with("auto-compact-") => { + break; + } + _ => {} + } + } + } + + let started_item = started_item.expect("context compaction item started"); + let completed_item = completed_item.expect("context compaction item completed"); + assert_eq!(started_item.id, completed_item.id); + assert!(legacy_event); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { skip_if_no_network!(); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 2fc5ba53c2..563aff7826 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -6,9 +6,12 @@ use anyhow::Result; use codex_core::CodexAuth; use codex_core::features::Feature; use codex_core::protocol::EventMsg; +use codex_core::protocol::ItemCompletedEvent; +use codex_core::protocol::ItemStartedEvent; use codex_core::protocol::Op; use codex_core::protocol::RolloutItem; use codex_core::protocol::RolloutLine; +use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; @@ -201,13 +204,13 @@ async fn remote_compact_runs_automatically() -> Result<()> { final_output_json_schema: None, }) .await?; - let message = wait_for_event_match(&codex, |ev| match ev { + + let message = wait_for_event_match(&codex, |event| match event { EventMsg::ContextCompacted(_) => Some(true), _ => None, }) .await; - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - + wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert!(message); assert_eq!(compact_mock.requests().len(), 1); let follow_up_body = responses_mock.single_request().body_json().to_string(); @@ -217,6 +220,101 @@ async fn remote_compact_runs_automatically() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { + skip_if_no_network!(Ok(())); + + let harness = TestCodexHarness::with_builder( + test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(|config| { + config.features.enable(Feature::RemoteCompaction); + }), + ) + .await?; + let codex = harness.test().codex.clone(); + + mount_sse_once( + harness.server(), + sse(vec![ + responses::ev_assistant_message("m1", "REMOTE_REPLY"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let compacted_history = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "REMOTE_COMPACTED_SUMMARY".to_string(), + }], + end_turn: None, + }, + ResponseItem::Compaction { + encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), + }, + ]; + let compact_mock = responses::mount_compact_json_once( + harness.server(), + serde_json::json!({ "output": compacted_history.clone() }), + ) + .await; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "manual remote compact".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await?; + wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + + codex.submit(Op::Compact).await?; + + let mut started_item = None; + let mut completed_item = None; + let mut legacy_event = false; + let mut saw_turn_complete = false; + + while !saw_turn_complete || started_item.is_none() || completed_item.is_none() || !legacy_event + { + let event = codex.next_event().await.unwrap(); + match event.msg { + EventMsg::ItemStarted(ItemStartedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + started_item = Some(item); + } + EventMsg::ItemCompleted(ItemCompletedEvent { + item: TurnItem::ContextCompaction(item), + .. + }) => { + completed_item = Some(item); + } + EventMsg::ContextCompacted(_) => { + legacy_event = true; + } + EventMsg::TurnComplete(_) => { + saw_turn_complete = true; + } + _ => {} + } + } + + let started_item = started_item.expect("context compaction item started"); + let completed_item = completed_item.expect("context compaction item completed"); + assert_eq!(started_item.id, completed_item.id); + assert!(legacy_event); + assert_eq!(compact_mock.requests().len(), 1); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/tests/suite/deprecation_notice.rs b/codex-rs/core/tests/suite/deprecation_notice.rs index d401a64cd4..06795cc51b 100644 --- a/codex-rs/core/tests/suite/deprecation_notice.rs +++ b/codex-rs/core/tests/suite/deprecation_notice.rs @@ -16,6 +16,7 @@ use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; +use std::collections::BTreeMap; use toml::Value as TomlValue; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -110,3 +111,73 @@ async fn emits_deprecation_notice_for_experimental_instructions_file() -> anyhow Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn emits_deprecation_notice_for_web_search_feature_flags() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let mut builder = test_codex().with_config(|config| { + let mut entries = BTreeMap::new(); + entries.insert("web_search_request".to_string(), true); + config.features.apply_map(&entries); + }); + + let TestCodex { codex, .. } = builder.build(&server).await?; + + let notice = wait_for_event_match(&codex, |event| match event { + EventMsg::DeprecationNotice(ev) if ev.summary.contains("[features].web_search_request") => { + Some(ev.clone()) + } + _ => None, + }) + .await; + + let DeprecationNoticeEvent { summary, details } = notice; + assert_eq!( + summary, + "`[features].web_search_request` is deprecated. Use `web_search` instead.".to_string(), + ); + assert_eq!( + details.as_deref(), + Some("Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` in config.toml."), + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn emits_deprecation_notice_for_disabled_web_search_feature_flag() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + + let mut builder = test_codex().with_config(|config| { + let mut entries = BTreeMap::new(); + entries.insert("web_search_request".to_string(), false); + config.features.apply_map(&entries); + }); + + let TestCodex { codex, .. } = builder.build(&server).await?; + + let notice = wait_for_event_match(&codex, |event| match event { + EventMsg::DeprecationNotice(ev) if ev.summary.contains("[features].web_search_request") => { + Some(ev.clone()) + } + _ => None, + }) + .await; + + let DeprecationNoticeEvent { summary, details } = notice; + assert_eq!( + summary, + "`[features].web_search_request` is deprecated. Use `web_search` instead.".to_string(), + ); + assert_eq!( + details.as_deref(), + Some("Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` in config.toml."), + ); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index c093482157..cdf597a4e9 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -10,6 +10,7 @@ use codex_core::exec::process_exec_tool_call; use codex_core::protocol::SandboxPolicy; use codex_core::sandboxing::SandboxPermissions; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; +use codex_protocol::config_types::WindowsSandboxLevel; use tempfile::TempDir; use codex_core::error::Result; @@ -27,7 +28,7 @@ fn skip_test() -> bool { #[expect(clippy::expect_used)] async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result { - let sandbox_type = get_platform_sandbox().expect("should be able to get sandbox type"); + let sandbox_type = get_platform_sandbox(false).expect("should be able to get sandbox type"); assert_eq!(sandbox_type, SandboxType::MacosSeatbelt); let params = ExecParams { @@ -36,6 +37,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result Vec { .with_model(model) // Keep tool expectations stable when the default web_search mode changes. .with_config(|config| { - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); config.features.enable(Feature::CollaborationModes); }); let test = builder diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index d995446864..14469c9643 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -118,6 +118,7 @@ async fn override_turn_context_records_permissions_update() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -161,6 +162,7 @@ async fn override_turn_context_records_environment_update() -> Result<()> { cwd: Some(new_cwd.path().to_path_buf()), approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -198,6 +200,7 @@ async fn override_turn_context_records_collaboration_update() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index f3d9e8d47b..e4eded15ff 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -4,6 +4,8 @@ use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; +use codex_execpolicy::Policy; +use codex_protocol::models::DeveloperInstructions; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::ev_completed; @@ -106,6 +108,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -227,6 +230,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -309,6 +313,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -408,10 +413,11 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; + let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.sandbox_policy = Constrained::allow_any(sandbox_policy); + config.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); }); let test = builder.build(&server).await?; @@ -429,39 +435,14 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let body = req.single_request().body_json(); let input = body["input"].as_array().expect("input array"); let permissions = permissions_texts(input); - let sandbox_text = "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."; - let approval_text = " Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-request`: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task.\n\nHere are scenarios where you'll need to request approval:\n- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)\n- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.\n- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)\n- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters - do not message the user before requesting approval for the command.\n- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.\n\nWhen requesting approval to execute a command that will require escalated privileges:\n - Provide the `sandbox_permissions` parameter with the value `\"require_escalated\"`\n - Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter"; - // Normalize paths by removing trailing slashes to match AbsolutePathBuf behavior - let normalize_path = - |p: &std::path::Path| -> String { p.to_string_lossy().trim_end_matches('/').to_string() }; - let mut roots = vec![ - normalize_path(writable.path()), - normalize_path(test.config.cwd.as_path()), - ]; - if cfg!(unix) && std::path::Path::new("/tmp").is_dir() { - roots.push("/tmp".to_string()); - } - if let Some(tmpdir) = std::env::var_os("TMPDIR") { - let tmpdir_path = std::path::PathBuf::from(&tmpdir); - if tmpdir_path.is_absolute() && !tmpdir.is_empty() { - roots.push(normalize_path(&tmpdir_path)); - } - } - let roots_text = if roots.len() == 1 { - format!(" The writable root is `{}`.", roots[0]) - } else { - format!( - " The writable roots are {}.", - roots - .iter() - .map(|root| format!("`{root}`")) - .collect::>() - .join(", ") - ) - }; - let expected = format!( - "{sandbox_text}{approval_text}{roots_text}" - ); + let expected = DeveloperInstructions::from_policy( + &sandbox_policy, + AskForApproval::OnRequest, + &Policy::empty(), + false, + test.config.cwd.as_path(), + ) + .into_text(); // Normalize line endings to handle Windows vs Unix differences let normalize_line_endings = |s: &str| s.replace("\r\n", "\n"); let expected_normalized = normalize_line_endings(&expected); diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 410ca08e4e..7a27866569 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -210,6 +210,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -362,6 +363,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(remote_slug.to_string()), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 32969bcf5a..579a1856d4 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -92,7 +92,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { config.user_instructions = Some("be consistent and helpful".to_string()); config.model = Some("gpt-5.1-codex-max".to_string()); // Keep tool expectations stable when the default web_search mode changes. - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); config.features.enable(Feature::CollaborationModes); }) .build(&server) @@ -350,6 +350,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(new_policy.clone()), + windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), @@ -427,6 +428,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: Some("gpt-5.1-codex".to_string()), effort: Some(Some(ReasoningEffort::Low)), summary: None, diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 7e5fab9871..71374cb84f 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -138,6 +138,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), effort: None, summary: None, @@ -367,6 +368,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model.to_string()), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index de62bf462d..cfc4a80488 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -819,6 +819,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { cwd: Some(repo_path.to_path_buf()), approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs new file mode 100644 index 0000000000..df6fa2ef72 --- /dev/null +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -0,0 +1,199 @@ +use anyhow::Result; +use codex_core::features::Feature; +use codex_protocol::ThreadId; +use codex_protocol::protocol::EventMsg; +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::UserMessageEvent; +use codex_state::STATE_DB_FILENAME; +use core_test_support::load_sse_fixture_with_id; +use core_test_support::responses::mount_sse_sequence; +use core_test_support::responses::start_mock_server; +use core_test_support::test_codex::test_codex; +use pretty_assertions::assert_eq; +use std::fs; +use tokio::time::Duration; +use uuid::Uuid; + +fn sse_completed(id: &str) -> String { + load_sse_fixture_with_id("../fixtures/completed_template.json", id) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn new_thread_is_recorded_in_state_db() -> Result<()> { + let server = start_mock_server().await; + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::Sqlite); + }); + let test = builder.build(&server).await?; + + let thread_id = test.session_configured.session_id; + let rollout_path = test.codex.rollout_path().expect("rollout path"); + let db_path = test.config.codex_home.join(STATE_DB_FILENAME); + + for _ in 0..100 { + if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let db = test.codex.state_db().expect("state db enabled"); + + let mut metadata = None; + for _ in 0..100 { + metadata = db.get_thread(thread_id).await?; + if metadata.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let metadata = metadata.expect("thread should exist in state db"); + assert_eq!(metadata.id, thread_id); + assert_eq!(metadata.rollout_path, rollout_path); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn backfill_scans_existing_rollouts() -> Result<()> { + let server = start_mock_server().await; + + let uuid = Uuid::now_v7(); + let thread_id = ThreadId::from_string(&uuid.to_string())?; + let rollout_rel_path = format!("sessions/2026/01/27/rollout-2026-01-27T12-00-00-{uuid}.jsonl"); + let rollout_rel_path_for_hook = rollout_rel_path.clone(); + + let mut builder = test_codex() + .with_pre_build_hook(move |codex_home| { + let rollout_path = codex_home.join(&rollout_rel_path_for_hook); + let parent = rollout_path + .parent() + .expect("rollout path should have parent"); + fs::create_dir_all(parent).expect("should create rollout directory"); + + let session_meta_line = SessionMetaLine { + meta: SessionMeta { + id: thread_id, + forked_from_id: None, + timestamp: "2026-01-27T12:00:00Z".to_string(), + cwd: codex_home.to_path_buf(), + originator: "test".to_string(), + cli_version: "test".to_string(), + source: SessionSource::default(), + model_provider: None, + base_instructions: None, + }, + git: None, + }; + + let lines = [ + RolloutLine { + timestamp: "2026-01-27T12:00:00Z".to_string(), + item: RolloutItem::SessionMeta(session_meta_line), + }, + RolloutLine { + timestamp: "2026-01-27T12:00:01Z".to_string(), + item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + message: "hello from backfill".to_string(), + images: None, + local_images: Vec::new(), + text_elements: Vec::new(), + })), + }, + ]; + + let jsonl = lines + .iter() + .map(|line| serde_json::to_string(line).expect("rollout line should serialize")) + .collect::>() + .join("\n"); + fs::write(&rollout_path, format!("{jsonl}\n")).expect("should write rollout file"); + }) + .with_config(|config| { + config.features.enable(Feature::Sqlite); + }); + + let test = builder.build(&server).await?; + + let db_path = test.config.codex_home.join(STATE_DB_FILENAME); + let rollout_path = test.config.codex_home.join(&rollout_rel_path); + let default_provider = test.config.model_provider_id.clone(); + + for _ in 0..20 { + if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let db = test.codex.state_db().expect("state db enabled"); + + let mut metadata = None; + for _ in 0..40 { + metadata = db.get_thread(thread_id).await?; + if metadata.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let metadata = metadata.expect("backfilled thread should exist in state db"); + assert_eq!(metadata.id, thread_id); + assert_eq!(metadata.rollout_path, rollout_path); + assert_eq!(metadata.model_provider, default_provider); + assert!(metadata.has_user_event); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn user_messages_persist_in_state_db() -> Result<()> { + let server = start_mock_server().await; + mount_sse_sequence( + &server, + vec![sse_completed("resp-1"), sse_completed("resp-2")], + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::Sqlite); + }); + let test = builder.build(&server).await?; + + let db_path = test.config.codex_home.join(STATE_DB_FILENAME); + for _ in 0..100 { + if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + test.submit_turn("hello from sqlite").await?; + test.submit_turn("another message").await?; + + let db = test.codex.state_db().expect("state db enabled"); + let thread_id = test.session_configured.session_id; + + let mut metadata = None; + for _ in 0..100 { + metadata = db.get_thread(thread_id).await?; + if metadata + .as_ref() + .map(|entry| entry.has_user_event) + .unwrap_or(false) + { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let metadata = metadata.expect("thread should exist in state db"); + assert!(metadata.has_user_event); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/web_search_cached.rs b/codex-rs/core/tests/suite/web_search_cached.rs index 261efaf942..df8b7dbb5b 100644 --- a/codex-rs/core/tests/suite/web_search_cached.rs +++ b/codex-rs/core/tests/suite/web_search_cached.rs @@ -1,6 +1,7 @@ #![allow(clippy::unwrap_used)] use codex_core::features::Feature; +use codex_core::protocol::SandboxPolicy; use codex_protocol::config_types::WebSearchMode; use core_test_support::load_sse_fixture_with_id; use core_test_support::responses; @@ -25,7 +26,7 @@ fn find_web_search_tool(body: &Value) -> &Value { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() { +async fn web_search_mode_cached_sets_external_web_access_false() { skip_if_no_network!(); let server = start_mock_server().await; @@ -35,7 +36,7 @@ async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() let mut builder = test_codex() .with_model("gpt-5-codex") .with_config(|config| { - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); }); let test = builder .build(&server) @@ -56,7 +57,7 @@ async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { +async fn web_search_mode_takes_precedence_over_legacy_flags() { skip_if_no_network!(); let server = start_mock_server().await; @@ -67,7 +68,7 @@ async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { .with_model("gpt-5-codex") .with_config(|config| { config.features.enable(Feature::WebSearchRequest); - config.web_search_mode = WebSearchMode::Cached; + config.web_search_mode = Some(WebSearchMode::Cached); }); let test = builder .build(&server) @@ -86,3 +87,90 @@ async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() { "web_search mode should win over legacy web_search_request" ); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_mode_defaults_to_cached_when_unset() { + skip_if_no_network!(); + + let server = start_mock_server().await; + let sse = sse_completed("resp-1"); + let resp_mock = responses::mount_sse_once(&server, sse).await; + + let mut builder = test_codex() + .with_model("gpt-5-codex") + .with_config(|config| { + config.web_search_mode = None; + config.features.disable(Feature::WebSearchCached); + config.features.disable(Feature::WebSearchRequest); + }); + let test = builder + .build(&server) + .await + .expect("create test Codex conversation"); + + test.submit_turn_with_policy("hello default cached web search", SandboxPolicy::ReadOnly) + .await + .expect("submit turn"); + + let body = resp_mock.single_request().body_json(); + let tool = find_web_search_tool(&body); + assert_eq!( + tool.get("external_web_access").and_then(Value::as_bool), + Some(false), + "default web_search should be cached when unset" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_mode_updates_between_turns_with_sandbox_policy() { + skip_if_no_network!(); + + let server = start_mock_server().await; + let resp_mock = responses::mount_sse_sequence( + &server, + vec![sse_completed("resp-1"), sse_completed("resp-2")], + ) + .await; + + let mut builder = test_codex() + .with_model("gpt-5-codex") + .with_config(|config| { + config.web_search_mode = None; + config.features.disable(Feature::WebSearchCached); + config.features.disable(Feature::WebSearchRequest); + }); + let test = builder + .build(&server) + .await + .expect("create test Codex conversation"); + + test.submit_turn_with_policy("hello cached", SandboxPolicy::ReadOnly) + .await + .expect("submit first turn"); + test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess) + .await + .expect("submit second turn"); + + let requests = resp_mock.requests(); + assert_eq!(requests.len(), 2, "expected two response requests"); + + let first_body = requests[0].body_json(); + let first_tool = find_web_search_tool(&first_body); + assert_eq!( + first_tool + .get("external_web_access") + .and_then(Value::as_bool), + Some(false), + "read-only policy should default web_search to cached" + ); + + let second_body = requests[1].body_json(); + let second_tool = find_web_search_tool(&second_body); + assert_eq!( + second_tool + .get("external_web_access") + .and_then(Value::as_bool), + Some(true), + "danger-full-access policy should default web_search to live" + ); +} diff --git a/codex-rs/exec-server/src/posix/escalate_server.rs b/codex-rs/exec-server/src/posix/escalate_server.rs index d99f300704..ef991ce8cd 100644 --- a/codex-rs/exec-server/src/posix/escalate_server.rs +++ b/codex-rs/exec-server/src/posix/escalate_server.rs @@ -10,6 +10,7 @@ use path_absolutize::Absolutize as _; use codex_core::SandboxState; use codex_core::exec::process_exec_tool_call; +use codex_core::protocol_config_types::WindowsSandboxLevel; use codex_core::sandboxing::SandboxPermissions; use tokio::process::Command; use tokio_util::sync::CancellationToken; @@ -87,6 +88,7 @@ impl EscalateServer { expiration: ExecExpiration::Cancellation(cancel_rx), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }, diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 56ff805fa0..4c10bb0d72 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -244,3 +244,37 @@ pub enum Color { #[default] Auto, } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn resume_parses_prompt_after_global_flags() { + const PROMPT: &str = "echo resume-with-global-flags-after-subcommand"; + let cli = Cli::parse_from([ + "codex-exec", + "resume", + "--last", + "--json", + "--model", + "gpt-5.2-codex", + "--dangerously-bypass-approvals-and-sandbox", + "--skip-git-repo-check", + PROMPT, + ]); + + let Some(Command::Resume(args)) = cli.command else { + panic!("expected resume command"); + }; + let effective_prompt = args.prompt.clone().or_else(|| { + if args.last { + args.session_id.clone() + } else { + None + } + }); + assert_eq!(effective_prompt.as_deref(), Some(PROMPT)); + } +} diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 0dfce33cb6..d43ce4387c 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -849,16 +849,17 @@ impl EventProcessor for EventProcessorWithJsonOutput { let protocol::Event { msg, .. } = event; - if let protocol::EventMsg::TurnComplete(protocol::TurnCompleteEvent { - last_agent_message, - }) = msg - { - if let Some(output_file) = self.last_message_path.as_deref() { - handle_last_message(last_agent_message.as_deref(), output_file); + match msg { + protocol::EventMsg::TurnComplete(protocol::TurnCompleteEvent { + last_agent_message, + }) => { + if let Some(output_file) = self.last_message_path.as_deref() { + handle_last_message(last_agent_message.as_deref(), output_file); + } + CodexStatus::InitiateShutdown } - CodexStatus::InitiateShutdown - } else { - CodexStatus::Running + protocol::EventMsg::ShutdownComplete => CodexStatus::Shutdown, + _ => CodexStatus::Running, } } } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 441041aa68..f2c212d895 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -46,13 +46,17 @@ use codex_utils_absolute_path::AbsolutePathBuf; use event_processor_with_human_output::EventProcessorWithHumanOutput; use event_processor_with_jsonl_output::EventProcessorWithJsonOutput; use serde_json::Value; +use std::collections::HashSet; use std::io::IsTerminal; use std::io::Read; use std::path::PathBuf; +use std::sync::Arc; use supports_color::Stream; +use tokio::sync::Mutex; use tracing::debug; use tracing::error; use tracing::info; +use tracing::warn; use tracing_subscriber::EnvFilter; use tracing_subscriber::prelude::*; @@ -72,6 +76,13 @@ enum InitialOperation { }, } +#[derive(Clone)] +struct ThreadEventEnvelope { + thread_id: codex_protocol::ThreadId, + thread: Arc, + event: Event, +} + pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { if let Err(err) = set_default_originator("codex_exec".to_string()) { tracing::warn!(?err, "Failed to set codex exec originator override {err:?}"); @@ -326,11 +337,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any true, config.cli_auth_credentials_store_mode, ); - let thread_manager = ThreadManager::new( + let thread_manager = Arc::new(ThreadManager::new( config.codex_home.clone(), auth_manager.clone(), SessionSource::Exec, - ); + )); let default_model = thread_manager .get_models_manager() .get_default_model(&config.model, &config, RefreshStrategy::OnlineIfUncached) @@ -338,7 +349,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // Handle resume subcommand by resolving a rollout path and using explicit resume API. let NewThread { - thread_id: _, + thread_id: primary_thread_id, thread, session_configured, } = if let Some(ExecCommand::Resume(args)) = command.as_ref() { @@ -420,40 +431,47 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any info!("Codex initialized with event: {session_configured:?}"); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let attached_threads = Arc::new(Mutex::new(HashSet::from([primary_thread_id]))); + spawn_thread_listener(primary_thread_id, thread.clone(), tx.clone()); + { let thread = thread.clone(); + tokio::spawn(async move { + if tokio::signal::ctrl_c().await.is_ok() { + tracing::debug!("Keyboard interrupt"); + // Immediately notify Codex to abort any in-flight task. + thread.submit(Op::Interrupt).await.ok(); + } + }); + } + + { + let thread_manager = Arc::clone(&thread_manager); + let attached_threads = Arc::clone(&attached_threads); + let tx = tx.clone(); + let mut thread_created_rx = thread_manager.subscribe_thread_created(); tokio::spawn(async move { loop { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - tracing::debug!("Keyboard interrupt"); - // Immediately notify Codex to abort any in‑flight task. - thread.submit(Op::Interrupt).await.ok(); - - // Exit the inner loop and return to the main input prompt. The codex - // will emit a `TurnInterrupted` (Error) event which is drained later. - break; - } - res = thread.next_event() => match res { - Ok(event) => { - debug!("Received event: {event:?}"); - - let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete); - if let Err(e) = tx.send(event) { - error!("Error sending event: {e:?}"); - break; + match thread_created_rx.recv().await { + Ok(thread_id) => { + if attached_threads.lock().await.contains(&thread_id) { + continue; + } + match thread_manager.get_thread(thread_id).await { + Ok(thread) => { + attached_threads.lock().await.insert(thread_id); + spawn_thread_listener(thread_id, thread, tx.clone()); } - if is_shutdown_complete { - info!("Received shutdown event, exiting event loop."); - break; + Err(err) => { + warn!("failed to attach listener for thread {thread_id}: {err}") } - }, - Err(e) => { - error!("Error receiving event: {e:?}"); - break; } } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + warn!("thread_created receiver lagged; skipping resync"); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } }); @@ -492,7 +510,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // Track whether a fatal error was reported by the server so we can // exit with a non-zero status for automation-friendly signaling. let mut error_seen = false; - while let Some(event) = rx.recv().await { + while let Some(envelope) = rx.recv().await { + let ThreadEventEnvelope { + thread_id, + thread, + event, + } = envelope; if let EventMsg::ElicitationRequest(ev) = &event.msg { // Automatically cancel elicitation requests in exec mode. thread @@ -506,15 +529,20 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any if matches!(event.msg, EventMsg::Error(_)) { error_seen = true; } - let shutdown: CodexStatus = event_processor.process_event(event); + if thread_id != primary_thread_id && matches!(&event.msg, EventMsg::TurnComplete(_)) { + continue; + } + let shutdown = event_processor.process_event(event); + if thread_id != primary_thread_id && matches!(shutdown, CodexStatus::InitiateShutdown) { + continue; + } match shutdown { CodexStatus::Running => continue, CodexStatus::InitiateShutdown => { thread.submit(Op::Shutdown).await?; } - CodexStatus::Shutdown => { - break; - } + CodexStatus::Shutdown if thread_id == primary_thread_id => break, + CodexStatus::Shutdown => continue, } } event_processor.print_final_output(); @@ -525,6 +553,42 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any Ok(()) } +fn spawn_thread_listener( + thread_id: codex_protocol::ThreadId, + thread: Arc, + tx: tokio::sync::mpsc::UnboundedSender, +) { + tokio::spawn(async move { + loop { + match thread.next_event().await { + Ok(event) => { + debug!("Received event: {event:?}"); + + let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete); + if let Err(err) = tx.send(ThreadEventEnvelope { + thread_id, + thread: Arc::clone(&thread), + event, + }) { + error!("Error sending event: {err:?}"); + break; + } + if is_shutdown_complete { + info!( + "Received shutdown event for thread {thread_id}, exiting event loop." + ); + break; + } + } + Err(err) => { + error!("Error receiving event: {err:?}"); + break; + } + } + } + }); +} + async fn resolve_resume_path( config: &Config, args: &crate::cli::ResumeArgs, diff --git a/codex-rs/exec/src/main.rs b/codex-rs/exec/src/main.rs index 03ee533ea9..2d3db1f42e 100644 --- a/codex-rs/exec/src/main.rs +++ b/codex-rs/exec/src/main.rs @@ -38,3 +38,44 @@ fn main() -> anyhow::Result<()> { Ok(()) }) } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn top_cli_parses_resume_prompt_after_config_flag() { + const PROMPT: &str = "echo resume-with-global-flags-after-subcommand"; + let cli = TopCli::parse_from([ + "codex-exec", + "resume", + "--last", + "--json", + "--model", + "gpt-5.2-codex", + "--config", + "reasoning_level=xhigh", + "--dangerously-bypass-approvals-and-sandbox", + "--skip-git-repo-check", + PROMPT, + ]); + + let Some(codex_exec::Command::Resume(args)) = cli.inner.command else { + panic!("expected resume command"); + }; + let effective_prompt = args.prompt.clone().or_else(|| { + if args.last { + args.session_id.clone() + } else { + None + } + }); + assert_eq!(effective_prompt.as_deref(), Some(PROMPT)); + assert_eq!(cli.config_overrides.raw_overrides.len(), 1); + assert_eq!( + cli.config_overrides.raw_overrides[0], + "reasoning_level=xhigh" + ); + } +} diff --git a/codex-rs/execpolicy/src/policy.rs b/codex-rs/execpolicy/src/policy.rs index 1e758277b8..0da0332d0b 100644 --- a/codex-rs/execpolicy/src/policy.rs +++ b/codex-rs/execpolicy/src/policy.rs @@ -31,6 +31,30 @@ impl Policy { &self.rules_by_program } + pub fn get_allowed_prefixes(&self) -> Vec> { + let mut prefixes = Vec::new(); + + for (_program, rules) in self.rules_by_program.iter_all() { + for rule in rules { + let Some(prefix_rule) = rule.as_any().downcast_ref::() else { + continue; + }; + if prefix_rule.decision != Decision::Allow { + continue; + } + + let mut prefix = Vec::with_capacity(prefix_rule.pattern.rest.len() + 1); + prefix.push(prefix_rule.pattern.first.as_ref().to_string()); + prefix.extend(prefix_rule.pattern.rest.iter().map(render_pattern_token)); + prefixes.push(prefix); + } + } + + prefixes.sort(); + prefixes.dedup(); + prefixes + } + pub fn add_prefix_rule(&mut self, prefix: &[String], decision: Decision) -> Result<()> { let (first_token, rest) = prefix .split_first() @@ -116,6 +140,13 @@ impl Policy { } } +fn render_pattern_token(token: &PatternToken) -> String { + match token { + PatternToken::Single(value) => value.clone(), + PatternToken::Alts(alternatives) => format!("[{}]", alternatives.join("|")), + } +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Evaluation { diff --git a/codex-rs/execpolicy/src/rule.rs b/codex-rs/execpolicy/src/rule.rs index de78a5fda9..b7c1a7cfde 100644 --- a/codex-rs/execpolicy/src/rule.rs +++ b/codex-rs/execpolicy/src/rule.rs @@ -96,6 +96,8 @@ pub trait Rule: Any + Debug + Send + Sync { fn program(&self) -> &str; fn matches(&self, cmd: &[String]) -> Option; + + fn as_any(&self) -> &dyn Any; } pub type RuleRef = Arc; @@ -114,6 +116,10 @@ impl Rule for PrefixRule { justification: self.justification.clone(), }) } + + fn as_any(&self) -> &dyn Any { + self + } } /// Count how many rules match each provided example and error if any example is unmatched. diff --git a/codex-rs/file-search/Cargo.toml b/codex-rs/file-search/Cargo.toml index 70ddcf2bb6..3802ed5fe3 100644 --- a/codex-rs/file-search/Cargo.toml +++ b/codex-rs/file-search/Cargo.toml @@ -15,11 +15,13 @@ path = "src/lib.rs" [dependencies] anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } +crossbeam-channel = { workspace = true } ignore = { workspace = true } -nucleo-matcher = { workspace = true } +nucleo = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } [dev-dependencies] pretty_assertions = { workspace = true } +tempfile = { workspace = true } diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index d55eb929f3..39255fa36e 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -1,38 +1,54 @@ +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use crossbeam_channel::after; +use crossbeam_channel::never; +use crossbeam_channel::select; +use crossbeam_channel::unbounded; use ignore::WalkBuilder; use ignore::overrides::OverrideBuilder; -use nucleo_matcher::Matcher; -use nucleo_matcher::Utf32Str; -use nucleo_matcher::pattern::AtomKind; -use nucleo_matcher::pattern::CaseMatching; -use nucleo_matcher::pattern::Normalization; -use nucleo_matcher::pattern::Pattern; +use nucleo::Config; +use nucleo::Injector; +use nucleo::Matcher; +use nucleo::Nucleo; +use nucleo::Utf32String; +use nucleo::pattern::CaseMatching; +use nucleo::pattern::Normalization; use serde::Serialize; -use std::cell::UnsafeCell; -use std::cmp::Reverse; -use std::collections::BinaryHeap; use std::num::NonZero; use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; +use std::sync::Condvar; +use std::sync::Mutex; +use std::sync::RwLock; use std::sync::atomic::AtomicBool; -use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; use tokio::process::Command; +#[cfg(test)] +use nucleo::Utf32Str; +#[cfg(test)] +use nucleo::pattern::AtomKind; +#[cfg(test)] +use nucleo::pattern::Pattern; + mod cli; pub use cli::Cli; /// A single match result returned from the search. /// -/// * `score` – Relevance score returned by `nucleo_matcher`. +/// * `score` – Relevance score returned by `nucleo`. /// * `path` – Path to the matched file (relative to the search directory). /// * `indices` – Optional list of character indices that matched the query. /// These are only filled when the caller of [`run`] sets /// `compute_indices` to `true`. The indices vector follows the -/// guidance from `nucleo_matcher::Pattern::indices`: they are +/// guidance from `nucleo::pattern::Pattern::indices`: they are /// unique and sorted in ascending order so that callers can use /// them directly for highlighting. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct FileMatch { pub score: u32, pub path: String, @@ -54,6 +70,127 @@ pub struct FileSearchResults { pub total_match_count: usize, } +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Default)] +pub struct FileSearchSnapshot { + pub query: String, + pub matches: Vec, + pub total_match_count: usize, + pub scanned_file_count: usize, + pub walk_complete: bool, +} + +#[derive(Debug, Clone)] +pub struct SessionOptions { + pub limit: NonZero, + pub exclude: Vec, + pub threads: NonZero, + pub compute_indices: bool, + pub respect_gitignore: bool, +} + +impl Default for SessionOptions { + fn default() -> Self { + Self { + #[expect(clippy::unwrap_used)] + limit: NonZero::new(20).unwrap(), + exclude: Vec::new(), + #[expect(clippy::unwrap_used)] + threads: NonZero::new(2).unwrap(), + compute_indices: false, + respect_gitignore: true, + } + } +} + +pub trait SessionReporter: Send + Sync + 'static { + /// Called when the debounced top-N changes. + fn on_update(&self, snapshot: &FileSearchSnapshot); + + /// Called when the session becomes idle or is cancelled. Guaranteed to be called at least once per update_query. + fn on_complete(&self); +} + +pub struct FileSearchSession { + inner: Arc, +} + +impl FileSearchSession { + /// Update the query. This should be cheap relative to re-walking. + pub fn update_query(&self, pattern_text: &str) { + let _ = self + .inner + .work_tx + .send(WorkSignal::QueryUpdated(pattern_text.to_string())); + } +} + +impl Drop for FileSearchSession { + fn drop(&mut self) { + self.inner.shutdown.store(true, Ordering::Relaxed); + let _ = self.inner.work_tx.send(WorkSignal::Shutdown); + } +} + +pub fn create_session( + search_directory: &Path, + options: SessionOptions, + reporter: Arc, +) -> anyhow::Result { + create_session_inner(search_directory, options, reporter, None) +} + +fn create_session_inner( + search_directory: &Path, + options: SessionOptions, + reporter: Arc, + cancel_flag: Option>, +) -> anyhow::Result { + let SessionOptions { + limit, + exclude, + threads, + compute_indices, + respect_gitignore, + } = options; + + let override_matcher = build_override_matcher(search_directory, &exclude)?; + let (work_tx, work_rx) = unbounded(); + + let notify_tx = work_tx.clone(); + let notify = Arc::new(move || { + let _ = notify_tx.send(WorkSignal::NucleoNotify); + }); + let nucleo = Nucleo::new( + Config::DEFAULT.match_paths(), + notify, + Some(threads.get()), + 1, + ); + let injector = nucleo.injector(); + + let cancelled = cancel_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false))); + + let inner = Arc::new(SessionInner { + search_directory: search_directory.to_path_buf(), + limit: limit.get(), + threads: threads.get(), + compute_indices, + respect_gitignore, + cancelled: cancelled.clone(), + shutdown: Arc::new(AtomicBool::new(false)), + reporter, + work_tx: work_tx.clone(), + }); + + let matcher_inner = inner.clone(); + thread::spawn(move || matcher_worker(matcher_inner, work_rx, nucleo)); + + let walker_inner = inner.clone(); + thread::spawn(move || walker_worker(walker_inner, override_matcher, injector)); + + Ok(FileSearchSession { inner }) +} + pub trait Reporter { fn report_match(&self, file_match: &FileMatch); fn warn_matches_truncated(&self, total_match_count: usize, shown_match_count: usize); @@ -142,172 +279,31 @@ pub fn run( compute_indices: bool, respect_gitignore: bool, ) -> anyhow::Result { - let pattern = create_pattern(pattern_text); - // Create one BestMatchesList per worker thread so that each worker can - // operate independently. The results across threads will be merged when - // the traversal is complete. - let WorkerCount { - num_walk_builder_threads, - num_best_matches_lists, - } = create_worker_count(threads); - let best_matchers_per_worker: Vec> = (0..num_best_matches_lists) - .map(|_| { - UnsafeCell::new(BestMatchesList::new( - limit.get(), - pattern.clone(), - Matcher::new(nucleo_matcher::Config::DEFAULT), - )) - }) - .collect(); + let reporter = Arc::new(RunReporter::default()); + let session = create_session_inner( + search_directory, + SessionOptions { + limit, + exclude, + threads, + compute_indices, + respect_gitignore, + }, + reporter.clone(), + Some(cancel_flag), + )?; - // Use the same tree-walker library that ripgrep uses. We use it directly so - // that we can leverage the parallelism it provides. - let mut walk_builder = WalkBuilder::new(search_directory); - walk_builder - .threads(num_walk_builder_threads) - // Allow hidden entries. - .hidden(false) - // Follow symlinks to search their contents. - .follow_links(true) - // Don't require git to be present to apply to apply git-related ignore rules. - .require_git(false); - if !respect_gitignore { - walk_builder - .git_ignore(false) - .git_global(false) - .git_exclude(false) - .ignore(false) - .parents(false); - } - - if !exclude.is_empty() { - let mut override_builder = OverrideBuilder::new(search_directory); - for exclude in exclude { - // The `!` prefix is used to indicate an exclude pattern. - let exclude_pattern = format!("!{exclude}"); - override_builder.add(&exclude_pattern)?; - } - let override_matcher = override_builder.build()?; - walk_builder.overrides(override_matcher); - } - let walker = walk_builder.build_parallel(); - - // Each worker created by `WalkParallel::run()` will have its own - // `BestMatchesList` to update. - let index_counter = AtomicUsize::new(0); - walker.run(|| { - let index = index_counter.fetch_add(1, Ordering::Relaxed); - let best_list_ptr = best_matchers_per_worker[index].get(); - let best_list = unsafe { &mut *best_list_ptr }; - - // Each worker keeps a local counter so we only read the atomic flag - // every N entries which is cheaper than checking on every file. - const CHECK_INTERVAL: usize = 1024; - let mut processed = 0; - - let cancel = cancel_flag.clone(); - - Box::new(move |entry| { - if let Some(path) = get_file_path(&entry, search_directory) { - best_list.insert(path); - } - - processed += 1; - if processed % CHECK_INTERVAL == 0 && cancel.load(Ordering::Relaxed) { - ignore::WalkState::Quit - } else { - ignore::WalkState::Continue - } - }) - }); - - fn get_file_path<'a>( - entry_result: &'a Result, - search_directory: &std::path::Path, - ) -> Option<&'a str> { - let entry = match entry_result { - Ok(e) => e, - Err(_) => return None, - }; - if entry.file_type().is_some_and(|ft| ft.is_dir()) { - return None; - } - let path = entry.path(); - match path.strip_prefix(search_directory) { - Ok(rel_path) => rel_path.to_str(), - Err(_) => None, - } - } - - // If the cancel flag is set, we return early with an empty result. - if cancel_flag.load(Ordering::Relaxed) { - return Ok(FileSearchResults { - matches: Vec::new(), - total_match_count: 0, - }); - } - - // Merge results across best_matchers_per_worker. - let mut global_heap: BinaryHeap> = BinaryHeap::new(); - let mut total_match_count = 0; - for best_list_cell in best_matchers_per_worker.iter() { - let best_list = unsafe { &*best_list_cell.get() }; - total_match_count += best_list.num_matches; - for &Reverse((score, ref line)) in best_list.binary_heap.iter() { - if global_heap.len() < limit.get() { - global_heap.push(Reverse((score, line.clone()))); - } else if let Some(min_element) = global_heap.peek() - && score > min_element.0.0 - { - global_heap.pop(); - global_heap.push(Reverse((score, line.clone()))); - } - } - } - - let mut raw_matches: Vec<(u32, String)> = global_heap.into_iter().map(|r| r.0).collect(); - sort_matches(&mut raw_matches); - - // Transform into `FileMatch`, optionally computing indices. - let mut matcher = if compute_indices { - Some(Matcher::new(nucleo_matcher::Config::DEFAULT)) - } else { - None - }; - - let matches: Vec = raw_matches - .into_iter() - .map(|(score, path)| { - let indices = if compute_indices { - let mut buf = Vec::::new(); - let haystack: Utf32Str<'_> = Utf32Str::new(&path, &mut buf); - let mut idx_vec: Vec = Vec::new(); - if let Some(ref mut m) = matcher { - // Ignore the score returned from indices – we already have `score`. - pattern.indices(haystack, m, &mut idx_vec); - } - idx_vec.sort_unstable(); - idx_vec.dedup(); - Some(idx_vec) - } else { - None - }; - - FileMatch { - score, - path, - indices, - } - }) - .collect(); + session.update_query(pattern_text); + let snapshot = reporter.wait_for_complete(); Ok(FileSearchResults { - matches, - total_match_count, + matches: snapshot.matches, + total_match_count: snapshot.total_match_count, }) } /// Sort matches in-place by descending score, then ascending path. +#[cfg(test)] fn sort_matches(matches: &mut [(u32, String)]) { matches.sort_by(cmp_by_score_desc_then_path_asc::<(u32, String), _, _>( |t| t.0, @@ -332,73 +328,7 @@ where } } -/// Maintains the `max_count` best matches for a given pattern. -struct BestMatchesList { - max_count: usize, - num_matches: usize, - pattern: Pattern, - matcher: Matcher, - binary_heap: BinaryHeap>, - - /// Internal buffer for converting strings to UTF-32. - utf32buf: Vec, -} - -impl BestMatchesList { - fn new(max_count: usize, pattern: Pattern, matcher: Matcher) -> Self { - Self { - max_count, - num_matches: 0, - pattern, - matcher, - binary_heap: BinaryHeap::new(), - utf32buf: Vec::::new(), - } - } - - fn insert(&mut self, line: &str) { - let haystack: Utf32Str<'_> = Utf32Str::new(line, &mut self.utf32buf); - if let Some(score) = self.pattern.score(haystack, &mut self.matcher) { - // In the tests below, we verify that score() returns None for a - // non-match, so we can categorically increment the count here. - self.num_matches += 1; - - if self.binary_heap.len() < self.max_count { - self.binary_heap.push(Reverse((score, line.to_string()))); - } else if let Some(min_element) = self.binary_heap.peek() - && score > min_element.0.0 - { - self.binary_heap.pop(); - self.binary_heap.push(Reverse((score, line.to_string()))); - } - } - } -} - -struct WorkerCount { - num_walk_builder_threads: usize, - num_best_matches_lists: usize, -} - -fn create_worker_count(num_workers: NonZero) -> WorkerCount { - // It appears that the number of times the function passed to - // `WalkParallel::run()` is called is: the number of threads specified to - // the builder PLUS ONE. - // - // In `WalkParallel::visit()`, the builder function gets called once here: - // https://github.com/BurntSushi/ripgrep/blob/79cbe89deb1151e703f4d91b19af9cdcc128b765/crates/ignore/src/walk.rs#L1233 - // - // And then once for every worker here: - // https://github.com/BurntSushi/ripgrep/blob/79cbe89deb1151e703f4d91b19af9cdcc128b765/crates/ignore/src/walk.rs#L1288 - let num_walk_builder_threads = num_workers.get(); - let num_best_matches_lists = num_walk_builder_threads + 1; - - WorkerCount { - num_walk_builder_threads, - num_best_matches_lists, - } -} - +#[cfg(test)] fn create_pattern(pattern: &str) -> Pattern { Pattern::new( pattern, @@ -408,16 +338,281 @@ fn create_pattern(pattern: &str) -> Pattern { ) } +struct SessionInner { + search_directory: PathBuf, + limit: usize, + threads: usize, + compute_indices: bool, + respect_gitignore: bool, + cancelled: Arc, + shutdown: Arc, + reporter: Arc, + work_tx: Sender, +} + +enum WorkSignal { + QueryUpdated(String), + NucleoNotify, + WalkComplete, + Shutdown, +} + +fn build_override_matcher( + search_directory: &Path, + exclude: &[String], +) -> anyhow::Result> { + if exclude.is_empty() { + return Ok(None); + } + let mut override_builder = OverrideBuilder::new(search_directory); + for exclude in exclude { + let exclude_pattern = format!("!{exclude}"); + override_builder.add(&exclude_pattern)?; + } + let matcher = override_builder.build()?; + Ok(Some(matcher)) +} + +fn walker_worker( + inner: Arc, + override_matcher: Option, + injector: Injector>, +) { + let mut walk_builder = WalkBuilder::new(&inner.search_directory); + walk_builder + .threads(inner.threads) + // Allow hidden entries. + .hidden(false) + // Follow symlinks to search their contents. + .follow_links(true) + // Don't require git to be present to apply to apply git-related ignore rules. + .require_git(false); + if !inner.respect_gitignore { + walk_builder + .git_ignore(false) + .git_global(false) + .git_exclude(false) + .ignore(false) + .parents(false); + } + if let Some(override_matcher) = override_matcher { + walk_builder.overrides(override_matcher); + } + + let walker = walk_builder.build_parallel(); + + fn get_file_path<'a>( + entry_result: &'a Result, + search_directory: &Path, + ) -> Option<&'a str> { + let entry = match entry_result { + Ok(entry) => entry, + Err(_) => return None, + }; + if entry.file_type().is_some_and(|ft| ft.is_dir()) { + return None; + } + let path = entry.path(); + match path.strip_prefix(search_directory) { + Ok(rel_path) => rel_path.to_str(), + Err(_) => None, + } + } + + walker.run(|| { + const CHECK_INTERVAL: usize = 1024; + let mut n = 0; + let search_directory = inner.search_directory.clone(); + let injector = injector.clone(); + let cancelled = inner.cancelled.clone(); + let shutdown = inner.shutdown.clone(); + + Box::new(move |entry| { + if let Some(path) = get_file_path(&entry, &search_directory) { + injector.push(Arc::from(path), |path, cols| { + cols[0] = Utf32String::from(path.as_ref()); + }); + } + n += 1; + if n >= CHECK_INTERVAL { + if cancelled.load(Ordering::Relaxed) || shutdown.load(Ordering::Relaxed) { + return ignore::WalkState::Quit; + } + n = 0; + } + ignore::WalkState::Continue + }) + }); + let _ = inner.work_tx.send(WorkSignal::WalkComplete); +} + +fn matcher_worker( + inner: Arc, + work_rx: Receiver, + mut nucleo: Nucleo>, +) -> anyhow::Result<()> { + const TICK_TIMEOUT_MS: u64 = 10; + let config = Config::DEFAULT.match_paths(); + let mut indices_matcher = inner.compute_indices.then(|| Matcher::new(config.clone())); + let cancel_requested = || inner.cancelled.load(Ordering::Relaxed); + let shutdown_requested = || inner.shutdown.load(Ordering::Relaxed); + + let mut last_query = String::new(); + let mut next_notify = never(); + let mut will_notify = false; + let mut walk_complete = false; + + loop { + select! { + recv(work_rx) -> signal => { + let Ok(signal) = signal else { + break; + }; + match signal { + WorkSignal::QueryUpdated(query) => { + let append = query.starts_with(&last_query); + nucleo.pattern.reparse( + 0, + &query, + CaseMatching::Smart, + Normalization::Smart, + append, + ); + last_query = query; + will_notify = true; + next_notify = after(Duration::from_millis(0)); + } + WorkSignal::NucleoNotify => { + if !will_notify { + will_notify = true; + next_notify = after(Duration::from_millis(TICK_TIMEOUT_MS)); + } + } + WorkSignal::WalkComplete => { + walk_complete = true; + if !will_notify { + will_notify = true; + next_notify = after(Duration::from_millis(0)); + } + } + WorkSignal::Shutdown => { + break; + } + } + } + recv(next_notify) -> _ => { + will_notify = false; + let status = nucleo.tick(TICK_TIMEOUT_MS); + if status.changed { + let snapshot = nucleo.snapshot(); + let limit = inner.limit.min(snapshot.matched_item_count() as usize); + let pattern = snapshot.pattern().column_pattern(0); + let matches: Vec<_> = snapshot + .matches() + .iter() + .take(limit) + .filter_map(|match_| { + let item = snapshot.get_item(match_.idx)?; + let indices = if let Some(indices_matcher) = indices_matcher.as_mut() { + let mut idx_vec = Vec::::new(); + let haystack = item.matcher_columns[0].slice(..); + let _ = pattern.indices(haystack, indices_matcher, &mut idx_vec); + idx_vec.sort_unstable(); + idx_vec.dedup(); + Some(idx_vec) + } else { + None + }; + Some(FileMatch { + score: match_.score, + path: item.data.as_ref().to_string(), + indices, + }) + }) + .collect(); + + let snapshot = FileSearchSnapshot { + query: last_query.clone(), + matches, + total_match_count: snapshot.matched_item_count() as usize, + scanned_file_count: snapshot.item_count() as usize, + walk_complete, + }; + inner.reporter.on_update(&snapshot); + } + if !status.running && walk_complete { + inner.reporter.on_complete(); + } + } + default(Duration::from_millis(100)) => { + // Occasionally check the cancel flag. + } + } + + if cancel_requested() || shutdown_requested() { + break; + } + } + + // If we cancelled or otherwise exited the loop, make sure the reporter is notified. + inner.reporter.on_complete(); + + Ok(()) +} + +#[derive(Default)] +struct RunReporter { + snapshot: RwLock, + completed: (Condvar, Mutex), +} + +impl SessionReporter for RunReporter { + fn on_update(&self, snapshot: &FileSearchSnapshot) { + #[expect(clippy::unwrap_used)] + let mut guard = self.snapshot.write().unwrap(); + *guard = snapshot.clone(); + } + + fn on_complete(&self) { + let (cv, mutex) = &self.completed; + let mut completed = mutex.lock().unwrap(); + *completed = true; + cv.notify_all(); + } +} + +impl RunReporter { + fn wait_for_complete(&self) -> FileSearchSnapshot { + let (cv, mutex) = &self.completed; + let mut completed = mutex.lock().unwrap(); + while !*completed { + completed = cv.wait(completed).unwrap(); + } + self.snapshot.read().unwrap().clone() + } +} + #[cfg(test)] mod tests { + #![allow(clippy::unwrap_used)] + use super::*; use pretty_assertions::assert_eq; + use std::fs; + use std::sync::Arc; + use std::sync::Condvar; + use std::sync::Mutex; + use std::sync::atomic::AtomicBool; + use std::thread; + use std::time::Duration; + use std::time::Instant; + use tempfile::TempDir; #[test] fn verify_score_is_none_for_non_match() { let mut utf32buf = Vec::::new(); let line = "hello"; - let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT); + let mut matcher = Matcher::new(Config::DEFAULT); let haystack: Utf32Str<'_> = Utf32Str::new(line, &mut utf32buf); let pattern = create_pattern("zzz"); let score = pattern.score(haystack, &mut matcher); @@ -453,4 +648,274 @@ mod tests { fn file_name_from_path_falls_back_to_full_path() { assert_eq!(file_name_from_path(""), ""); } + + #[derive(Default)] + struct RecordingReporter { + updates: Mutex>, + complete_times: Mutex>, + complete_cv: Condvar, + update_cv: Condvar, + } + + impl RecordingReporter { + fn wait_for_complete(&self, timeout: Duration) -> bool { + let completes = self.complete_times.lock().unwrap(); + if !completes.is_empty() { + return true; + } + let (completes, _) = self.complete_cv.wait_timeout(completes, timeout).unwrap(); + !completes.is_empty() + } + fn clear(&self) { + self.updates.lock().unwrap().clear(); + self.complete_times.lock().unwrap().clear(); + } + + fn updates(&self) -> Vec { + self.updates.lock().unwrap().clone() + } + + fn wait_for_updates_at_least(&self, min_len: usize, timeout: Duration) -> bool { + let updates = self.updates.lock().unwrap(); + if updates.len() >= min_len { + return true; + } + let (updates, _) = self.update_cv.wait_timeout(updates, timeout).unwrap(); + updates.len() >= min_len + } + + fn snapshot(&self) -> FileSearchSnapshot { + self.updates + .lock() + .unwrap() + .last() + .cloned() + .unwrap_or_default() + } + } + + impl SessionReporter for RecordingReporter { + fn on_update(&self, snapshot: &FileSearchSnapshot) { + let mut updates = self.updates.lock().unwrap(); + updates.push(snapshot.clone()); + self.update_cv.notify_all(); + } + + fn on_complete(&self) { + { + let mut complete_times = self.complete_times.lock().unwrap(); + complete_times.push(Instant::now()); + } + self.complete_cv.notify_all(); + } + } + + fn create_temp_tree(file_count: usize) -> TempDir { + let dir = tempfile::tempdir().unwrap(); + for i in 0..file_count { + let path = dir.path().join(format!("file-{i:04}.txt")); + fs::write(path, format!("contents {i}")).unwrap(); + } + dir + } + + #[test] + fn session_scanned_file_count_is_monotonic_across_queries() { + let dir = create_temp_tree(200); + let reporter = Arc::new(RecordingReporter::default()); + let session = create_session(dir.path(), SessionOptions::default(), reporter.clone()) + .expect("session"); + + session.update_query("file-00"); + thread::sleep(Duration::from_millis(20)); + let first_snapshot = reporter.snapshot(); + session.update_query("file-01"); + thread::sleep(Duration::from_millis(20)); + let second_snapshot = reporter.snapshot(); + let _ = reporter.wait_for_complete(Duration::from_secs(5)); + let completed_snapshot = reporter.snapshot(); + + assert!(second_snapshot.scanned_file_count >= first_snapshot.scanned_file_count); + assert!(completed_snapshot.scanned_file_count >= second_snapshot.scanned_file_count); + } + + #[test] + fn session_streams_updates_before_walk_complete() { + let dir = create_temp_tree(600); + let reporter = Arc::new(RecordingReporter::default()); + let session = create_session(dir.path(), SessionOptions::default(), reporter.clone()) + .expect("session"); + + session.update_query("file-0"); + let completed = reporter.wait_for_complete(Duration::from_secs(5)); + + assert!(completed); + let updates = reporter.updates(); + assert!(updates.iter().any(|snapshot| !snapshot.walk_complete)); + } + + #[test] + fn session_accepts_query_updates_after_walk_complete() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("alpha.txt"), "alpha").unwrap(); + fs::write(dir.path().join("beta.txt"), "beta").unwrap(); + let reporter = Arc::new(RecordingReporter::default()); + let session = create_session(dir.path(), SessionOptions::default(), reporter.clone()) + .expect("session"); + + session.update_query("alpha"); + assert!(reporter.wait_for_complete(Duration::from_secs(5))); + let updates_before = reporter.updates().len(); + + session.update_query("beta"); + assert!(reporter.wait_for_updates_at_least(updates_before + 1, Duration::from_secs(5),)); + + let updates = reporter.updates(); + let last_update = updates.last().cloned().expect("update"); + assert!( + last_update + .matches + .iter() + .any(|file_match| file_match.path.contains("beta.txt")) + ); + } + + #[test] + fn session_emits_complete_when_query_changes_with_no_matches() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("alpha.txt"), "alpha").unwrap(); + fs::write(dir.path().join("beta.txt"), "beta").unwrap(); + let reporter = Arc::new(RecordingReporter::default()); + let session = create_session_inner( + dir.path(), + SessionOptions::default(), + reporter.clone(), + None, + ) + .expect("session"); + + session.update_query("asdf"); + assert!(reporter.wait_for_complete(Duration::from_secs(5))); + + let completed_snapshot = reporter.snapshot(); + assert_eq!(completed_snapshot.matches, Vec::new()); + assert_eq!(completed_snapshot.total_match_count, 0); + + reporter.clear(); + + session.update_query("asdfa"); + assert!(reporter.wait_for_complete(Duration::from_secs(5))); + assert!(!reporter.updates().is_empty()); + } + + #[test] + fn dropping_session_does_not_cancel_siblings_with_shared_cancel_flag() { + let root_a = create_temp_tree(200); + let root_b = create_temp_tree(4_000); + let cancel_flag = Arc::new(AtomicBool::new(false)); + + let reporter_a = Arc::new(RecordingReporter::default()); + let session_a = create_session_inner( + root_a.path(), + SessionOptions::default(), + reporter_a, + Some(cancel_flag.clone()), + ) + .expect("session_a"); + + let reporter_b = Arc::new(RecordingReporter::default()); + let session_b = create_session_inner( + root_b.path(), + SessionOptions::default(), + reporter_b.clone(), + Some(cancel_flag), + ) + .expect("session_b"); + + session_a.update_query("file-0"); + session_b.update_query("file-1"); + + thread::sleep(Duration::from_millis(5)); + drop(session_a); + + let completed = reporter_b.wait_for_complete(Duration::from_secs(5)); + assert_eq!(completed, true); + } + + #[test] + fn session_emits_updates_when_query_changes() { + let dir = create_temp_tree(200); + let reporter = Arc::new(RecordingReporter::default()); + let session = create_session(dir.path(), SessionOptions::default(), reporter.clone()) + .expect("session"); + + session.update_query("zzzzzzzz"); + let completed = reporter.wait_for_complete(Duration::from_secs(5)); + assert!(completed); + + reporter.clear(); + + session.update_query("zzzzzzzzq"); + let completed = reporter.wait_for_complete(Duration::from_secs(5)); + assert!(completed); + + let updates = reporter.updates(); + assert_eq!(updates.len(), 1); + } + + #[test] + fn run_returns_matches_for_query() { + let dir = create_temp_tree(40); + let results = run( + "file-000", + NonZero::new(20).unwrap(), + dir.path(), + Vec::new(), + NonZero::new(2).unwrap(), + Arc::new(AtomicBool::new(false)), + false, + true, + ) + .expect("run ok"); + + assert!(!results.matches.is_empty()); + assert!(results.total_match_count >= results.matches.len()); + assert!( + results + .matches + .iter() + .any(|m| m.path.contains("file-0000.txt")) + ); + } + + #[test] + fn cancel_exits_run() { + let dir = create_temp_tree(200); + let cancel_flag = Arc::new(AtomicBool::new(true)); + let search_dir = dir.path().to_path_buf(); + let (tx, rx) = std::sync::mpsc::channel(); + + let handle = thread::spawn(move || { + let result = run( + "file-", + NonZero::new(20).unwrap(), + &search_dir, + Vec::new(), + NonZero::new(2).unwrap(), + cancel_flag, + false, + true, + ); + let _ = tx.send(result); + }); + + let result = rx + .recv_timeout(Duration::from_secs(2)) + .expect("run should exit after cancellation"); + handle.join().unwrap(); + + let results = result.expect("run ok"); + assert_eq!(results.matches, Vec::new()); + assert_eq!(results.total_match_count, 0); + } } diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 663f3cbe21..84f89a1678 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -7,6 +7,7 @@ use codex_core::exec::ExecParams; use codex_core::exec::process_exec_tool_call; use codex_core::exec_env::create_env; use codex_core::protocol::SandboxPolicy; +use codex_core::protocol_config_types::WindowsSandboxLevel; use codex_core::sandboxing::SandboxPermissions; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -60,6 +61,7 @@ async fn run_cmd_output( expiration: timeout_ms.into(), env: create_env_from_core_vars(), sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; @@ -177,6 +179,7 @@ async fn assert_network_blocked(cmd: &[&str]) { expiration: NETWORK_TIMEOUT_MS.into(), env: create_env_from_core_vars(), sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 2d303b1c95..959837d4e2 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -34,6 +34,7 @@ rama-core = { version = "=0.3.0-alpha.4" } rama-http = { version = "=0.3.0-alpha.4" } rama-http-backend = { version = "=0.3.0-alpha.4", features = ["tls"] } rama-net = { version = "=0.3.0-alpha.4", features = ["http", "tls"] } +rama-socks5 = { version = "=0.3.0-alpha.4" } rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] } rama-tls-boring = { version = "=0.3.0-alpha.4", features = ["http"] } diff --git a/codex-rs/network-proxy/README.md b/codex-rs/network-proxy/README.md index 1d19a92a68..3d8c20307d 100644 --- a/codex-rs/network-proxy/README.md +++ b/codex-rs/network-proxy/README.md @@ -3,6 +3,7 @@ `codex-network-proxy` is Codex's local network policy enforcement proxy. It runs: - an HTTP proxy (default `127.0.0.1:3128`) +- an optional SOCKS5 proxy (default `127.0.0.1:8081`, disabled by default) - an admin HTTP API (default `127.0.0.1:8080`) It enforces an allow/deny policy and a "limited" mode intended for read-only network access. @@ -20,6 +21,10 @@ Example config: enabled = true proxy_url = "http://127.0.0.1:3128" admin_url = "http://127.0.0.1:8080" +# Optional SOCKS5 listener (disabled by default). +enable_socks5 = false +socks_url = "http://127.0.0.1:8081" +enable_socks5_udp = false # When `enabled` is false, the proxy no-ops and does not bind listeners. # When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only), # including CONNECT tunnels in full mode. @@ -28,7 +33,7 @@ allow_upstream_proxy = false # If you want to expose these listeners beyond localhost, you must opt in explicitly. dangerously_allow_non_loopback_proxy = false dangerously_allow_non_loopback_admin = false -mode = "limited" # or "full" +mode = "full" # default when unset; use "limited" for read-only mode [network_proxy.policy] # Hosts must match the allowlist (unless denied). @@ -60,6 +65,12 @@ export HTTP_PROXY="http://127.0.0.1:3128" export HTTPS_PROXY="http://127.0.0.1:3128" ``` +For SOCKS5 traffic (when `enable_socks5 = true`): + +```bash +export ALL_PROXY="socks5h://127.0.0.1:8081" +``` + ### 4) Understand blocks / debugging When a request is blocked, the proxy responds with `403` and includes: @@ -70,8 +81,8 @@ When a request is blocked, the proxy responds with `403` and includes: - `blocked-by-method-policy` - `blocked-by-policy` -In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed for plain HTTP. HTTPS `CONNECT` -remains a transparent tunnel, so limited-mode method enforcement does not apply to HTTPS. +In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` and SOCKS5 are +blocked because they would bypass method enforcement. ## Library API diff --git a/codex-rs/network-proxy/src/config.rs b/codex-rs/network-proxy/src/config.rs index 7bd6957ac7..ae4a450179 100644 --- a/codex-rs/network-proxy/src/config.rs +++ b/codex-rs/network-proxy/src/config.rs @@ -23,6 +23,12 @@ pub struct NetworkProxySettings { #[serde(default = "default_admin_url")] pub admin_url: String, #[serde(default)] + pub enable_socks5: bool, + #[serde(default = "default_socks_url")] + pub socks_url: String, + #[serde(default)] + pub enable_socks5_udp: bool, + #[serde(default)] pub allow_upstream_proxy: bool, #[serde(default)] pub dangerously_allow_non_loopback_proxy: bool, @@ -40,6 +46,9 @@ impl Default for NetworkProxySettings { enabled: false, proxy_url: default_proxy_url(), admin_url: default_admin_url(), + enable_socks5: false, + socks_url: default_socks_url(), + enable_socks5_udp: false, allow_upstream_proxy: false, dangerously_allow_non_loopback_proxy: false, dangerously_allow_non_loopback_admin: false, @@ -90,6 +99,10 @@ fn default_admin_url() -> String { "http://127.0.0.1:8080".to_string() } +fn default_socks_url() -> String { + "http://127.0.0.1:8081".to_string() +} + /// Clamp non-loopback bind addresses to loopback unless explicitly allowed. fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr { if addr.ip().is_loopback() { @@ -110,21 +123,27 @@ fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> pub(crate) fn clamp_bind_addrs( http_addr: SocketAddr, + socks_addr: SocketAddr, admin_addr: SocketAddr, cfg: &NetworkProxySettings, -) -> (SocketAddr, SocketAddr) { +) -> (SocketAddr, SocketAddr, SocketAddr) { let http_addr = clamp_non_loopback( http_addr, cfg.dangerously_allow_non_loopback_proxy, "HTTP proxy", ); + let socks_addr = clamp_non_loopback( + socks_addr, + cfg.dangerously_allow_non_loopback_proxy, + "SOCKS5 proxy", + ); let admin_addr = clamp_non_loopback( admin_addr, cfg.dangerously_allow_non_loopback_admin, "admin API", ); if cfg.policy.allow_unix_sockets.is_empty() { - return (http_addr, admin_addr); + return (http_addr, socks_addr, admin_addr); } // `x-unix-socket` is intentionally a local escape hatch. If the proxy (or admin API) is @@ -136,6 +155,11 @@ pub(crate) fn clamp_bind_addrs( "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback" ); } + if cfg.dangerously_allow_non_loopback_proxy && !socks_addr.ip().is_loopback() { + warn!( + "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping SOCKS5 proxy to loopback" + ); + } if cfg.dangerously_allow_non_loopback_admin && !admin_addr.ip().is_loopback() { warn!( "unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_admin and clamping admin API to loopback" @@ -143,12 +167,14 @@ pub(crate) fn clamp_bind_addrs( } ( SocketAddr::from(([127, 0, 0, 1], http_addr.port())), + SocketAddr::from(([127, 0, 0, 1], socks_addr.port())), SocketAddr::from(([127, 0, 0, 1], admin_addr.port())), ) } pub struct RuntimeConfig { pub http_addr: SocketAddr, + pub socks_addr: SocketAddr, pub admin_addr: SocketAddr, } @@ -159,16 +185,24 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result { cfg.network_proxy.proxy_url ) })?; + let socks_addr = resolve_addr(&cfg.network_proxy.socks_url, 8081).with_context(|| { + format!( + "invalid network_proxy.socks_url: {}", + cfg.network_proxy.socks_url + ) + })?; let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080).with_context(|| { format!( "invalid network_proxy.admin_url: {}", cfg.network_proxy.admin_url ) })?; - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg.network_proxy); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg.network_proxy); Ok(RuntimeConfig { http_addr, + socks_addr, admin_addr, }) } @@ -403,11 +437,14 @@ mod tests { ..Default::default() }; let http_addr = "0.0.0.0:3128".parse::().unwrap(); + let socks_addr = "0.0.0.0:8081".parse::().unwrap(); let admin_addr = "0.0.0.0:8080".parse::().unwrap(); - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg); assert_eq!(http_addr, "0.0.0.0:3128".parse::().unwrap()); + assert_eq!(socks_addr, "0.0.0.0:8081".parse::().unwrap()); assert_eq!(admin_addr, "0.0.0.0:8080".parse::().unwrap()); } @@ -423,11 +460,14 @@ mod tests { ..Default::default() }; let http_addr = "0.0.0.0:3128".parse::().unwrap(); + let socks_addr = "0.0.0.0:8081".parse::().unwrap(); let admin_addr = "0.0.0.0:8080".parse::().unwrap(); - let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg); + let (http_addr, socks_addr, admin_addr) = + clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg); assert_eq!(http_addr, "127.0.0.1:3128".parse::().unwrap()); + assert_eq!(socks_addr, "127.0.0.1:8081".parse::().unwrap()); assert_eq!(admin_addr, "127.0.0.1:8080".parse::().unwrap()); } } diff --git a/codex-rs/network-proxy/src/http_proxy.rs b/codex-rs/network-proxy/src/http_proxy.rs index abe6b30144..b2856ac601 100644 --- a/codex-rs/network-proxy/src/http_proxy.rs +++ b/codex-rs/network-proxy/src/http_proxy.rs @@ -2,6 +2,7 @@ use crate::config::NetworkMode; use crate::network_policy::NetworkDecision; use crate::network_policy::NetworkPolicyDecider; use crate::network_policy::NetworkPolicyRequest; +use crate::network_policy::NetworkPolicyRequestArgs; use crate::network_policy::NetworkProtocol; use crate::network_policy::evaluate_host_policy; use crate::policy::normalize_host; @@ -12,6 +13,7 @@ use crate::responses::blocked_header_value; use crate::responses::json_response; use crate::runtime::unix_socket_permissions_supported; use crate::state::BlockedRequest; +use crate::state::BlockedRequestArgs; use crate::state::NetworkProxyState; use crate::upstream::UpstreamClient; use crate::upstream::proxy_for_connect; @@ -146,27 +148,27 @@ async fn http_connect_accept( .await); } - let request = NetworkPolicyRequest::new( - NetworkProtocol::HttpsConnect, - host.clone(), - authority.port, - client.clone(), - Some("CONNECT".to_string()), - None, - None, - ); + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::HttpsConnect, + host: host.clone(), + port: authority.port, + client_addr: client.clone(), + method: Some("CONNECT".to_string()), + command: None, + exec_policy_hint: None, + }); match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { Ok(NetworkDecision::Deny { reason }) => { let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - reason.clone(), - client.clone(), - Some("CONNECT".to_string()), - None, - "http-connect".to_string(), - )) + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: reason.clone(), + client: client.clone(), + method: Some("CONNECT".to_string()), + mode: None, + protocol: "http-connect".to_string(), + })) .await; let client = client.as_deref().unwrap_or_default(); warn!("CONNECT blocked (client={client}, host={host}, reason={reason})"); @@ -189,14 +191,14 @@ async fn http_connect_accept( if mode == NetworkMode::Limited { let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - REASON_METHOD_NOT_ALLOWED.to_string(), - client.clone(), - Some("CONNECT".to_string()), - Some(NetworkMode::Limited), - "http-connect".to_string(), - )) + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_METHOD_NOT_ALLOWED.to_string(), + client: client.clone(), + method: Some("CONNECT".to_string()), + mode: Some(NetworkMode::Limited), + protocol: "http-connect".to_string(), + })) .await; let client = client.as_deref().unwrap_or_default(); warn!("CONNECT blocked by method policy (client={client}, host={host}, mode=limited)"); @@ -425,27 +427,27 @@ async fn http_plain_proxy( .await); } - let request = NetworkPolicyRequest::new( - NetworkProtocol::Http, - host.clone(), + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Http, + host: host.clone(), port, - client.clone(), - Some(req.method().as_str().to_string()), - None, - None, - ); + client_addr: client.clone(), + method: Some(req.method().as_str().to_string()), + command: None, + exec_policy_hint: None, + }); match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { Ok(NetworkDecision::Deny { reason }) => { let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - reason.clone(), - client.clone(), - Some(req.method().as_str().to_string()), - None, - "http".to_string(), - )) + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: reason.clone(), + client: client.clone(), + method: Some(req.method().as_str().to_string()), + mode: None, + protocol: "http".to_string(), + })) .await; let client = client.as_deref().unwrap_or_default(); warn!("request blocked (client={client}, host={host}, reason={reason})"); @@ -460,14 +462,14 @@ async fn http_plain_proxy( if !method_allowed { let _ = app_state - .record_blocked(BlockedRequest::new( - host.clone(), - REASON_METHOD_NOT_ALLOWED.to_string(), - client.clone(), - Some(req.method().as_str().to_string()), - Some(NetworkMode::Limited), - "http".to_string(), - )) + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_METHOD_NOT_ALLOWED.to_string(), + client: client.clone(), + method: Some(req.method().as_str().to_string()), + mode: Some(NetworkMode::Limited), + protocol: "http".to_string(), + })) .await; let client = client.as_deref().unwrap_or_default(); let method = req.method(); @@ -565,14 +567,14 @@ async fn proxy_disabled_response( protocol: &str, ) -> Response { let _ = app_state - .record_blocked(BlockedRequest::new( + .record_blocked(BlockedRequest::new(BlockedRequestArgs { host, - REASON_PROXY_DISABLED.to_string(), + reason: REASON_PROXY_DISABLED.to_string(), client, method, - None, - protocol.to_string(), - )) + mode: None, + protocol: protocol.to_string(), + })) .await; text_response(StatusCode::SERVICE_UNAVAILABLE, "proxy disabled") } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 71e4ec7356..e636273128 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -9,6 +9,7 @@ mod proxy; mod reasons; mod responses; mod runtime; +mod socks5; mod state; mod upstream; @@ -16,6 +17,7 @@ use anyhow::Result; pub use network_policy::NetworkDecision; pub use network_policy::NetworkPolicyDecider; pub use network_policy::NetworkPolicyRequest; +pub use network_policy::NetworkPolicyRequestArgs; pub use network_policy::NetworkProtocol; pub use proxy::Args; pub use proxy::NetworkProxy; diff --git a/codex-rs/network-proxy/src/network_policy.rs b/codex-rs/network-proxy/src/network_policy.rs index 6f410b3e4b..f14202510c 100644 --- a/codex-rs/network-proxy/src/network_policy.rs +++ b/codex-rs/network-proxy/src/network_policy.rs @@ -26,16 +26,27 @@ pub struct NetworkPolicyRequest { pub exec_policy_hint: Option, } +pub struct NetworkPolicyRequestArgs { + pub protocol: NetworkProtocol, + pub host: String, + pub port: u16, + pub client_addr: Option, + pub method: Option, + pub command: Option, + pub exec_policy_hint: Option, +} + impl NetworkPolicyRequest { - pub fn new( - protocol: NetworkProtocol, - host: String, - port: u16, - client_addr: Option, - method: Option, - command: Option, - exec_policy_hint: Option, - ) -> Self { + pub fn new(args: NetworkPolicyRequestArgs) -> Self { + let NetworkPolicyRequestArgs { + protocol, + host, + port, + client_addr, + method, + command, + exec_policy_hint, + } = args; Self { protocol, host, @@ -139,15 +150,15 @@ mod tests { } }); - let request = NetworkPolicyRequest::new( - NetworkProtocol::Http, - "example.com".to_string(), - 80, - None, - Some("GET".to_string()), - None, - None, - ); + 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 = evaluate_host_policy(&state, Some(&decider), &request) .await @@ -172,15 +183,15 @@ mod tests { } }); - let request = NetworkPolicyRequest::new( - NetworkProtocol::Http, - "blocked.com".to_string(), - 80, - None, - Some("GET".to_string()), - None, - None, - ); + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Http, + host: "blocked.com".to_string(), + port: 80, + client_addr: None, + method: Some("GET".to_string()), + command: None, + exec_policy_hint: None, + }); let decision = evaluate_host_policy(&state, Some(&decider), &request) .await @@ -210,15 +221,15 @@ mod tests { } }); - let request = NetworkPolicyRequest::new( - NetworkProtocol::Http, - "127.0.0.1".to_string(), - 80, - None, - Some("GET".to_string()), - None, - None, - ); + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Http, + host: "127.0.0.1".to_string(), + port: 80, + client_addr: None, + method: Some("GET".to_string()), + command: None, + exec_policy_hint: None, + }); let decision = evaluate_host_policy(&state, Some(&decider), &request) .await diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 480880e946..cb09b9959d 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -3,6 +3,7 @@ use crate::config; use crate::http_proxy; use crate::network_policy::NetworkPolicyDecider; use crate::runtime::unix_socket_permissions_supported; +use crate::socks5; use crate::state::NetworkProxyState; use anyhow::Context; use anyhow::Result; @@ -61,8 +62,9 @@ impl NetworkProxyBuilder { let current_cfg = state.current_cfg().await?; let runtime = config::resolve_runtime(¤t_cfg)?; // Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only. - let (http_addr, admin_addr) = config::clamp_bind_addrs( + let (http_addr, socks_addr, admin_addr) = config::clamp_bind_addrs( self.http_addr.unwrap_or(runtime.http_addr), + runtime.socks_addr, self.admin_addr.unwrap_or(runtime.admin_addr), ¤t_cfg.network_proxy, ); @@ -70,6 +72,7 @@ impl NetworkProxyBuilder { Ok(NetworkProxy { state, http_addr, + socks_addr, admin_addr, policy_decider: self.policy_decider, }) @@ -80,6 +83,7 @@ impl NetworkProxyBuilder { pub struct NetworkProxy { state: Arc, http_addr: SocketAddr, + socks_addr: SocketAddr, admin_addr: SocketAddr, policy_decider: Option>, } @@ -105,10 +109,21 @@ impl NetworkProxy { self.http_addr, self.policy_decider.clone(), )); + let socks_task = if current_cfg.network_proxy.enable_socks5 { + Some(tokio::spawn(socks5::run_socks5( + self.state.clone(), + self.socks_addr, + self.policy_decider.clone(), + current_cfg.network_proxy.enable_socks5_udp, + ))) + } else { + None + }; let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr)); Ok(NetworkProxyHandle { http_task: Some(http_task), + socks_task, admin_task: Some(admin_task), completed: false, }) @@ -117,6 +132,7 @@ impl NetworkProxy { pub struct NetworkProxyHandle { http_task: Option>>, + socks_task: Option>>, admin_task: Option>>, completed: bool, } @@ -125,6 +141,7 @@ impl NetworkProxyHandle { fn noop() -> Self { Self { http_task: Some(tokio::spawn(async { Ok(()) })), + socks_task: None, admin_task: Some(tokio::spawn(async { Ok(()) })), completed: true, } @@ -133,33 +150,49 @@ impl NetworkProxyHandle { pub async fn wait(mut self) -> Result<()> { let http_task = self.http_task.take().context("missing http proxy task")?; let admin_task = self.admin_task.take().context("missing admin proxy task")?; + let socks_task = self.socks_task.take(); let http_result = http_task.await; let admin_result = admin_task.await; + let socks_result = match socks_task { + Some(task) => Some(task.await), + None => None, + }; self.completed = true; http_result??; admin_result??; + if let Some(socks_result) = socks_result { + socks_result??; + } Ok(()) } pub async fn shutdown(mut self) -> Result<()> { - abort_tasks(self.http_task.take(), self.admin_task.take()).await; + abort_tasks( + self.http_task.take(), + self.socks_task.take(), + self.admin_task.take(), + ) + .await; self.completed = true; Ok(()) } } +async fn abort_task(task: Option>>) { + if let Some(task) = task { + task.abort(); + let _ = task.await; + } +} + async fn abort_tasks( http_task: Option>>, + socks_task: Option>>, admin_task: Option>>, ) { - if let Some(http_task) = http_task { - http_task.abort(); - let _ = http_task.await; - } - if let Some(admin_task) = admin_task { - admin_task.abort(); - let _ = admin_task.await; - } + abort_task(http_task).await; + abort_task(socks_task).await; + abort_task(admin_task).await; } impl Drop for NetworkProxyHandle { @@ -168,9 +201,10 @@ impl Drop for NetworkProxyHandle { return; } let http_task = self.http_task.take(); + let socks_task = self.socks_task.take(); let admin_task = self.admin_task.take(); tokio::spawn(async move { - abort_tasks(http_task, admin_task).await; + abort_tasks(http_task, socks_task, admin_task).await; }); } } diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index 49e34d5c0a..1c29672cfa 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -73,15 +73,25 @@ pub struct BlockedRequest { pub timestamp: i64, } +pub struct BlockedRequestArgs { + pub host: String, + pub reason: String, + pub client: Option, + pub method: Option, + pub mode: Option, + pub protocol: String, +} + impl BlockedRequest { - pub fn new( - host: String, - reason: String, - client: Option, - method: Option, - mode: Option, - protocol: String, - ) -> Self { + pub fn new(args: BlockedRequestArgs) -> Self { + let BlockedRequestArgs { + host, + reason, + client, + method, + mode, + protocol, + } = args; Self { host, reason, diff --git a/codex-rs/network-proxy/src/socks5.rs b/codex-rs/network-proxy/src/socks5.rs new file mode 100644 index 0000000000..44be9060be --- /dev/null +++ b/codex-rs/network-proxy/src/socks5.rs @@ -0,0 +1,320 @@ +use crate::config::NetworkMode; +use crate::network_policy::NetworkDecision; +use crate::network_policy::NetworkPolicyDecider; +use crate::network_policy::NetworkPolicyRequest; +use crate::network_policy::NetworkPolicyRequestArgs; +use crate::network_policy::NetworkProtocol; +use crate::network_policy::evaluate_host_policy; +use crate::policy::normalize_host; +use crate::reasons::REASON_METHOD_NOT_ALLOWED; +use crate::reasons::REASON_PROXY_DISABLED; +use crate::state::BlockedRequest; +use crate::state::BlockedRequestArgs; +use crate::state::NetworkProxyState; +use anyhow::Context as _; +use anyhow::Result; +use rama_core::Layer; +use rama_core::Service; +use rama_core::error::BoxError; +use rama_core::extensions::ExtensionsRef; +use rama_core::layer::AddInputExtensionLayer; +use rama_core::service::service_fn; +use rama_net::client::EstablishedClientConnection; +use rama_net::stream::SocketInfo; +use rama_socks5::Socks5Acceptor; +use rama_socks5::server::DefaultConnector; +use rama_socks5::server::DefaultUdpRelay; +use rama_socks5::server::udp::RelayRequest; +use rama_socks5::server::udp::RelayResponse; +use rama_tcp::TcpStream; +use rama_tcp::client::Request as TcpRequest; +use rama_tcp::client::service::TcpConnector; +use rama_tcp::server::TcpListener; +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; +use tracing::error; +use tracing::info; +use tracing::warn; + +pub async fn run_socks5( + state: Arc, + addr: SocketAddr, + policy_decider: Option>, + enable_socks5_udp: bool, +) -> Result<()> { + let listener = TcpListener::build() + .bind(addr) + .await + // See `http_proxy.rs` for details on why we wrap `BoxError` before converting to anyhow. + .map_err(rama_core::error::OpaqueError::from) + .map_err(anyhow::Error::from) + .with_context(|| format!("bind SOCKS5 proxy: {addr}"))?; + + info!("SOCKS5 proxy listening on {addr}"); + + match state.network_mode().await { + Ok(NetworkMode::Limited) => { + info!("SOCKS5 is blocked in limited mode; set mode=\"full\" to allow SOCKS5"); + } + Ok(NetworkMode::Full) => {} + Err(err) => { + warn!("failed to read network mode: {err}"); + } + } + + let tcp_connector = TcpConnector::default(); + let policy_tcp_connector = service_fn({ + let policy_decider = policy_decider.clone(); + move |req: TcpRequest| { + let tcp_connector = tcp_connector.clone(); + let policy_decider = policy_decider.clone(); + async move { handle_socks5_tcp(req, tcp_connector, policy_decider).await } + } + }); + + let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector); + let base = Socks5Acceptor::new().with_connector(socks_connector); + + if enable_socks5_udp { + let udp_state = state.clone(); + let udp_decider = policy_decider.clone(); + let udp_relay = DefaultUdpRelay::default().with_async_inspector(service_fn({ + move |request: RelayRequest| { + let udp_state = udp_state.clone(); + let udp_decider = udp_decider.clone(); + async move { inspect_socks5_udp(request, udp_state, udp_decider).await } + } + })); + let socks_acceptor = base.with_udp_associator(udp_relay); + listener + .serve(AddInputExtensionLayer::new(state).into_layer(socks_acceptor)) + .await; + } else { + listener + .serve(AddInputExtensionLayer::new(state).into_layer(base)) + .await; + } + Ok(()) +} + +async fn handle_socks5_tcp( + req: TcpRequest, + tcp_connector: TcpConnector, + policy_decider: Option>, +) -> Result, BoxError> { + let app_state = req + .extensions() + .get::>() + .cloned() + .ok_or_else(|| io::Error::other("missing state"))?; + + let host = normalize_host(&req.authority.host.to_string()); + let port = req.authority.port; + if host.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host").into()); + } + + let client = req + .extensions() + .get::() + .map(|info| info.peer_addr().to_string()); + + match app_state.enabled().await { + Ok(true) => {} + Ok(false) => { + let _ = app_state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_PROXY_DISABLED.to_string(), + client: client.clone(), + method: None, + mode: None, + protocol: "socks5".to_string(), + })) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!("SOCKS blocked; proxy disabled (client={client}, host={host})"); + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "proxy disabled").into()); + } + Err(err) => { + error!("failed to read enabled state: {err}"); + return Err(io::Error::other("proxy error").into()); + } + } + + match app_state.network_mode().await { + Ok(NetworkMode::Limited) => { + let _ = app_state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_METHOD_NOT_ALLOWED.to_string(), + client: client.clone(), + method: None, + mode: Some(NetworkMode::Limited), + protocol: "socks5".to_string(), + })) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!( + "SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)" + ); + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into()); + } + Ok(NetworkMode::Full) => {} + Err(err) => { + error!("failed to evaluate method policy: {err}"); + return Err(io::Error::other("proxy error").into()); + } + } + + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Socks5Tcp, + host: host.clone(), + port, + client_addr: client.clone(), + method: None, + command: None, + exec_policy_hint: None, + }); + + match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { + let _ = app_state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: reason.clone(), + client: client.clone(), + method: None, + mode: None, + protocol: "socks5".to_string(), + })) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!("SOCKS blocked (client={client}, host={host}, reason={reason})"); + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into()); + } + Ok(NetworkDecision::Allow) => { + let client = client.as_deref().unwrap_or_default(); + info!("SOCKS allowed (client={client}, host={host}, port={port})"); + } + Err(err) => { + error!("failed to evaluate host: {err}"); + return Err(io::Error::other("proxy error").into()); + } + } + + tcp_connector.serve(req).await +} + +async fn inspect_socks5_udp( + request: RelayRequest, + state: Arc, + policy_decider: Option>, +) -> io::Result { + let RelayRequest { + server_address, + payload, + extensions, + .. + } = request; + + let host = normalize_host(&server_address.ip_addr.to_string()); + let port = server_address.port; + if host.is_empty() { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host")); + } + + let client = extensions + .get::() + .map(|info| info.peer_addr().to_string()); + + match state.enabled().await { + Ok(true) => {} + Ok(false) => { + let _ = state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_PROXY_DISABLED.to_string(), + client: client.clone(), + method: None, + mode: None, + protocol: "socks5-udp".to_string(), + })) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!("SOCKS UDP blocked; proxy disabled (client={client}, host={host})"); + return Ok(RelayResponse { + maybe_payload: None, + extensions, + }); + } + Err(err) => { + error!("failed to read enabled state: {err}"); + return Err(io::Error::other("proxy error")); + } + } + + match state.network_mode().await { + Ok(NetworkMode::Limited) => { + let _ = state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: REASON_METHOD_NOT_ALLOWED.to_string(), + client: client.clone(), + method: None, + mode: Some(NetworkMode::Limited), + protocol: "socks5-udp".to_string(), + })) + .await; + return Ok(RelayResponse { + maybe_payload: None, + extensions, + }); + } + Ok(NetworkMode::Full) => {} + Err(err) => { + error!("failed to evaluate method policy: {err}"); + return Err(io::Error::other("proxy error")); + } + } + + let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs { + protocol: NetworkProtocol::Socks5Udp, + host: host.clone(), + port, + client_addr: client.clone(), + method: None, + command: None, + exec_policy_hint: None, + }); + + match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await { + Ok(NetworkDecision::Deny { reason }) => { + let _ = state + .record_blocked(BlockedRequest::new(BlockedRequestArgs { + host: host.clone(), + reason: reason.clone(), + client: client.clone(), + method: None, + mode: None, + protocol: "socks5-udp".to_string(), + })) + .await; + let client = client.as_deref().unwrap_or_default(); + warn!("SOCKS UDP blocked (client={client}, host={host}, reason={reason})"); + Ok(RelayResponse { + maybe_payload: None, + extensions, + }) + } + Ok(NetworkDecision::Allow) => Ok(RelayResponse { + maybe_payload: Some(payload), + extensions, + }), + Err(err) => { + error!("failed to evaluate UDP host: {err}"); + Err(io::Error::other("proxy error")) + } + } +} diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index e1e8a1e4e6..552455f510 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -20,6 +20,7 @@ use serde::Deserialize; use std::collections::HashSet; pub use crate::runtime::BlockedRequest; +pub use crate::runtime::BlockedRequestArgs; pub use crate::runtime::NetworkProxyState; #[cfg(test)] pub(crate) use crate::runtime::network_proxy_state_for_policy; diff --git a/codex-rs/otel/src/metrics/mod.rs b/codex-rs/otel/src/metrics/mod.rs index b13d5f917e..30a7aa1cf6 100644 --- a/codex-rs/otel/src/metrics/mod.rs +++ b/codex-rs/otel/src/metrics/mod.rs @@ -17,6 +17,6 @@ pub(crate) fn install_global(metrics: MetricsClient) { let _ = GLOBAL_METRICS.set(metrics); } -pub(crate) fn global() -> Option { +pub fn global() -> Option { GLOBAL_METRICS.get().cloned() } diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 19ec1fcc36..c3d9d30915 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] codex-git = { workspace = true } +codex-execpolicy = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-image = { workspace = true } icu_decimal = { workspace = true } diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 00b9bc0e80..86d39d6d4a 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -66,6 +66,18 @@ pub enum SandboxMode { DangerFullAccess, } +#[derive( + Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS, +)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum WindowsSandboxLevel { + #[default] + Disabled, + RestrictedToken, + Elevated, +} + #[derive( Debug, Serialize, diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 12fa6a0f51..e7c916a61d 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -2,6 +2,7 @@ use crate::models::WebSearchAction; use crate::protocol::AgentMessageEvent; use crate::protocol::AgentReasoningEvent; use crate::protocol::AgentReasoningRawContentEvent; +use crate::protocol::ContextCompactedEvent; use crate::protocol::EventMsg; use crate::protocol::UserMessageEvent; use crate::protocol::WebSearchEndEvent; @@ -21,6 +22,7 @@ pub enum TurnItem { AgentMessage(AgentMessageItem), Reasoning(ReasoningItem), WebSearch(WebSearchItem), + ContextCompaction(ContextCompactionItem), } #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] @@ -57,6 +59,29 @@ pub struct WebSearchItem { pub action: WebSearchAction, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +pub struct ContextCompactionItem { + pub id: String, +} + +impl ContextCompactionItem { + pub fn new() -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + } + } + + pub fn as_legacy_event(&self) -> EventMsg { + EventMsg::ContextCompacted(ContextCompactedEvent {}) + } +} + +impl Default for ContextCompactionItem { + fn default() -> Self { + Self::new() + } +} + impl UserMessageItem { pub fn new(content: &[UserInput]) -> Self { Self { @@ -195,6 +220,7 @@ impl TurnItem { TurnItem::AgentMessage(item) => item.id.clone(), TurnItem::Reasoning(item) => item.id.clone(), TurnItem::WebSearch(item) => item.id.clone(), + TurnItem::ContextCompaction(item) => item.id.clone(), } } @@ -204,6 +230,7 @@ impl TurnItem { TurnItem::AgentMessage(item) => item.as_legacy_events(), TurnItem::WebSearch(item) => vec![item.as_legacy_event()], TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning), + TurnItem::ContextCompaction(item) => vec![item.as_legacy_event()], } } } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index b0b8156916..50e8ee342f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -19,6 +19,7 @@ use crate::protocol::NetworkAccess; use crate::protocol::SandboxPolicy; use crate::protocol::WritableRoot; use crate::user_input::UserInput; +use codex_execpolicy::Policy; use codex_git::GhostCommit; use codex_utils_image::error::ImageProcessingError; use schemars::JsonSchema; @@ -205,6 +206,8 @@ const APPROVAL_POLICY_ON_FAILURE: &str = include_str!("prompts/permissions/approval_policy/on_failure.md"); const APPROVAL_POLICY_ON_REQUEST: &str = include_str!("prompts/permissions/approval_policy/on_request.md"); +const APPROVAL_POLICY_ON_REQUEST_RULE: &str = + include_str!("prompts/permissions/approval_policy/on_request_rule.md"); const SANDBOX_MODE_DANGER_FULL_ACCESS: &str = include_str!("prompts/permissions/sandbox_mode/danger_full_access.md"); @@ -217,12 +220,42 @@ impl DeveloperInstructions { Self { text: text.into() } } + pub fn from( + approval_policy: AskForApproval, + exec_policy: &Policy, + request_rule_enabled: bool, + ) -> DeveloperInstructions { + let text = match approval_policy { + AskForApproval::Never => APPROVAL_POLICY_NEVER.to_string(), + AskForApproval::UnlessTrusted => APPROVAL_POLICY_UNLESS_TRUSTED.to_string(), + AskForApproval::OnFailure => APPROVAL_POLICY_ON_FAILURE.to_string(), + AskForApproval::OnRequest => { + if !request_rule_enabled { + APPROVAL_POLICY_ON_REQUEST.to_string() + } else { + let command_prefixes = format_allow_prefixes(exec_policy); + match command_prefixes { + Some(prefixes) => { + format!("{APPROVAL_POLICY_ON_REQUEST_RULE}\n{prefixes}") + } + None => APPROVAL_POLICY_ON_REQUEST_RULE.to_string(), + } + } + } + }; + + DeveloperInstructions::new(text) + } + pub fn into_text(self) -> String { self.text } pub fn concat(self, other: impl Into) -> Self { let mut text = self.text; + if !text.ends_with('\n') { + text.push('\n'); + } text.push_str(&other.into().text); Self { text } } @@ -237,6 +270,8 @@ impl DeveloperInstructions { pub fn from_policy( sandbox_policy: &SandboxPolicy, approval_policy: AskForApproval, + exec_policy: &Policy, + request_rule_enabled: bool, cwd: &Path, ) -> Self { let network_access = if sandbox_policy.has_full_network_access() { @@ -259,6 +294,8 @@ impl DeveloperInstructions { sandbox_mode, network_access, approval_policy, + exec_policy, + request_rule_enabled, writable_roots, ) } @@ -281,6 +318,8 @@ impl DeveloperInstructions { sandbox_mode: SandboxMode, network_access: NetworkAccess, approval_policy: AskForApproval, + exec_policy: &Policy, + request_rule_enabled: bool, writable_roots: Option>, ) -> Self { let start_tag = DeveloperInstructions::new(""); @@ -290,7 +329,11 @@ impl DeveloperInstructions { sandbox_mode, network_access, )) - .concat(DeveloperInstructions::from(approval_policy)) + .concat(DeveloperInstructions::from( + approval_policy, + exec_policy, + request_rule_enabled, + )) .concat(DeveloperInstructions::from_writable_roots(writable_roots)) .concat(end_tag) } @@ -328,6 +371,37 @@ impl DeveloperInstructions { } } +pub fn render_command_prefix_list(prefixes: I) -> Option +where + I: IntoIterator, + P: AsRef<[String]>, +{ + let lines = prefixes + .into_iter() + .map(|prefix| format!("- {}", render_command_prefix(prefix.as_ref()))) + .collect::>(); + if lines.is_empty() { + return None; + } + + Some(lines.join("\n")) +} + +fn render_command_prefix(prefix: &[String]) -> String { + let tokens = prefix + .iter() + .map(|token| serde_json::to_string(token).unwrap_or_else(|_| format!("{token:?}"))) + .collect::>() + .join(", "); + format!("[{tokens}]") +} + +fn format_allow_prefixes(exec_policy: &Policy) -> Option { + let prefixes = exec_policy.get_allowed_prefixes(); + let lines = render_command_prefix_list(prefixes)?; + Some(format!("Approved command prefixes:\n{lines}")) +} + impl From for ResponseItem { fn from(di: DeveloperInstructions) -> Self { ResponseItem::Message { @@ -352,19 +426,6 @@ impl From for DeveloperInstructions { } } -impl From for DeveloperInstructions { - fn from(mode: AskForApproval) -> Self { - let text = match mode { - AskForApproval::Never => APPROVAL_POLICY_NEVER.trim_end(), - AskForApproval::UnlessTrusted => APPROVAL_POLICY_UNLESS_TRUSTED.trim_end(), - AskForApproval::OnFailure => APPROVAL_POLICY_ON_FAILURE.trim_end(), - AskForApproval::OnRequest => APPROVAL_POLICY_ON_REQUEST.trim_end(), - }; - - DeveloperInstructions::new(text) - } -} - fn should_serialize_reasoning_content(content: &Option>) -> bool { match content { Some(content) => !content @@ -633,6 +694,10 @@ pub struct ShellToolCallParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub sandbox_permissions: Option, + /// Suggests a command prefix to persist for future sessions + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub prefix_rule: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -653,6 +718,9 @@ pub struct ShellCommandToolCallParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub sandbox_permissions: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub prefix_rule: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -836,6 +904,7 @@ mod tests { use crate::config_types::SandboxMode; use crate::protocol::AskForApproval; use anyhow::Result; + use codex_execpolicy::Policy; use mcp_types::ImageContent; use mcp_types::TextContent; use pretty_assertions::assert_eq; @@ -844,15 +913,17 @@ mod tests { #[test] fn converts_sandbox_mode_into_developer_instructions() { + let workspace_write: DeveloperInstructions = SandboxMode::WorkspaceWrite.into(); assert_eq!( - DeveloperInstructions::from(SandboxMode::WorkspaceWrite), + workspace_write, DeveloperInstructions::new( "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted." ) ); + let read_only: DeveloperInstructions = SandboxMode::ReadOnly.into(); assert_eq!( - DeveloperInstructions::from(SandboxMode::ReadOnly), + read_only, DeveloperInstructions::new( "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted." ) @@ -865,6 +936,8 @@ mod tests { SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, AskForApproval::OnRequest, + &Policy::empty(), + false, None, ); @@ -891,6 +964,8 @@ mod tests { let instructions = DeveloperInstructions::from_policy( &policy, AskForApproval::UnlessTrusted, + &Policy::empty(), + false, &PathBuf::from("/tmp"), ); let text = instructions.into_text(); @@ -898,6 +973,30 @@ mod tests { assert!(text.contains("`approval_policy` is `unless-trusted`")); } + #[test] + fn includes_request_rule_instructions_when_enabled() { + let mut exec_policy = Policy::empty(); + exec_policy + .add_prefix_rule( + &["git".to_string(), "pull".to_string()], + codex_execpolicy::Decision::Allow, + ) + .expect("add rule"); + let instructions = DeveloperInstructions::from_permissions_with_network( + SandboxMode::WorkspaceWrite, + NetworkAccess::Enabled, + AskForApproval::OnRequest, + &exec_policy, + true, + None, + ); + + let text = instructions.into_text(); + assert!(text.contains("prefix_rule")); + assert!(text.contains("Approved command prefixes")); + assert!(text.contains(r#"["git", "pull"]"#)); + } + #[test] fn serializes_success_as_plain_string() -> Result<()> { let item = ResponseInputItem::FunctionCallOutput { @@ -1126,6 +1225,7 @@ mod tests { workdir: Some("/tmp".to_string()), timeout_ms: Some(1000), sandbox_permissions: None, + prefix_rule: None, justification: None, }, params diff --git a/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md new file mode 100644 index 0000000000..336f1fb748 --- /dev/null +++ b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md @@ -0,0 +1,61 @@ +# Escalation Requests + +Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to: + +- Pipes: | +- Logical operators: &&, || +- Command separators: ; +- Subshell boundaries: (...), $(...) + +Each resulting segment is evaluated independently for sandbox restrictions and approval requirements. + +Example: + +git pull | tee output.txt + +This is treated as two command segments: + +["git", "pull"] + +["tee", "output.txt"] + +## How to request escalation + +IMPORTANT: To request approval to execute a command that will require escalated privileges: + +- Provide the `sandbox_permissions` parameter with the value `"require_escalated"` +- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?" +- Suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions. + +If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with "require_escalated". ALWAYS proceed to use the `justification` and `prefix_rule` parameters - do not message the user before requesting approval for the command. + +## When to request escalation + +While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox: + +- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var) +- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. +- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command. +- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for. + +Only run commands that require approval if it is absolutely necessary to solve the user's query, don't try and circumvent approvals by using other tools. + +## prefix_rule guidance + +When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You MUST NOT pass the entire command into `prefix_rule`. + + +["npm", "run", "dev"] + + +["gh", "pr", "checks"] + + +["pytest"] + + +["cargo", "test", "-p", "codex-app-server"] + +["cargo", "test"] + + diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index c60b13d72f..b9a4f2120b 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -16,6 +16,7 @@ use crate::approvals::ElicitationRequestEvent; use crate::config_types::CollaborationMode; 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::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; @@ -158,6 +159,10 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] sandbox_policy: Option, + /// Updated Windows sandbox mode for tool execution. + #[serde(skip_serializing_if = "Option::is_none")] + windows_sandbox_level: Option, + /// Updated model slug. When set, the model info is derived /// automatically. #[serde(skip_serializing_if = "Option::is_none")] @@ -2088,11 +2093,14 @@ pub struct SkillMetadata { pub description: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - /// Legacy short_description from SKILL.md. Prefer SKILL.toml interface.short_description. + /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. pub short_description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub interface: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub dependencies: Option, pub path: PathBuf, pub scope: SkillScope, pub enabled: bool, @@ -2114,6 +2122,31 @@ pub struct SkillInterface { pub default_prompt: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)] +pub struct SkillDependencies { + pub tools: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)] +pub struct SkillToolDependency { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub r#type: String, + pub value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub transport: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub command: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub url: Option, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct SkillErrorInfo { pub path: PathBuf, diff --git a/codex-rs/protocol/src/thread_id.rs b/codex-rs/protocol/src/thread_id.rs index 7b27db8364..8d6d96eff8 100644 --- a/codex-rs/protocol/src/thread_id.rs +++ b/codex-rs/protocol/src/thread_id.rs @@ -28,6 +28,28 @@ impl ThreadId { } } +impl TryFrom<&str> for ThreadId { + type Error = uuid::Error; + + fn try_from(value: &str) -> Result { + Self::from_string(value) + } +} + +impl TryFrom for ThreadId { + type Error = uuid::Error; + + fn try_from(value: String) -> Result { + Self::from_string(value.as_str()) + } +} + +impl From for String { + fn from(value: ThreadId) -> Self { + value.to_string() + } +} + impl Default for ThreadId { fn default() -> Self { Self::new() @@ -36,7 +58,7 @@ impl Default for ThreadId { impl Display for ThreadId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.uuid) + Display::fmt(&self.uuid, f) } } diff --git a/codex-rs/rmcp-client/src/bin/test_stdio_server.rs b/codex-rs/rmcp-client/src/bin/test_stdio_server.rs index 7805a7de9a..3719766acb 100644 --- a/codex-rs/rmcp-client/src/bin/test_stdio_server.rs +++ b/codex-rs/rmcp-client/src/bin/test_stdio_server.rs @@ -35,12 +35,19 @@ struct TestToolServer { const MEMO_URI: &str = "memo://codex/example-note"; const MEMO_CONTENT: &str = "This is a sample MCP resource served by the rmcp test server."; +const SMALL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; + pub fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout) { (tokio::io::stdin(), tokio::io::stdout()) } + impl TestToolServer { fn new() -> Self { - let tools = vec![Self::echo_tool(), Self::image_tool()]; + let tools = vec![ + Self::echo_tool(), + Self::image_tool(), + Self::image_scenario_tool(), + ]; let resources = vec![Self::memo_resource()]; let resource_templates = vec![Self::memo_template()]; Self { @@ -86,6 +93,61 @@ impl TestToolServer { ) } + /// Tool intended for manual testing of Codex TUI rendering for MCP image tool results. + /// + /// This exists to exercise edge cases where a `CallToolResult.content` includes image blocks + /// that aren't the first item (or includes invalid image blocks before a valid image). + /// + /// Manual testing approach (Codex TUI): + /// - Build this binary: `cargo build -p codex-rmcp-client --bin test_stdio_server` + /// - Register it: + /// - `codex mcp add mcpimg -- /abs/path/to/test_stdio_server` + /// - Then in Codex TUI, ask it to call: + /// - `mcpimg.image_scenario({"scenario":"image_only"})` + /// - `mcpimg.image_scenario({"scenario":"text_then_image","caption":"Here is the image:"})` + /// - `mcpimg.image_scenario({"scenario":"invalid_base64_then_image"})` + /// - `mcpimg.image_scenario({"scenario":"invalid_image_bytes_then_image"})` + /// - `mcpimg.image_scenario({"scenario":"multiple_valid_images"})` + /// - `mcpimg.image_scenario({"scenario":"image_then_text","caption":"Here is the image:"})` + /// - `mcpimg.image_scenario({"scenario":"text_only","caption":"Here is the image:"})` + /// - You should see an extra history cell: `tool result (image output)`. + fn image_scenario_tool() -> Tool { + #[expect(clippy::expect_used)] + let schema: JsonObject = serde_json::from_value(serde_json::json!({ + "type": "object", + "properties": { + "scenario": { + "type": "string", + "enum": [ + "image_only", + "text_then_image", + "invalid_base64_then_image", + "invalid_image_bytes_then_image", + "multiple_valid_images", + "image_then_text", + "text_only" + ] + }, + "caption": { "type": "string" }, + "data_url": { + "type": "string", + "description": "Optional data URL like data:image/png;base64,AAAA...; if omitted, uses a built-in tiny PNG." + } + }, + "required": ["scenario"], + "additionalProperties": false + })) + .expect("image_scenario tool schema should deserialize"); + + Tool::new( + Cow::Borrowed("image_scenario"), + Cow::Borrowed( + "Return content blocks for manual testing of MCP image rendering scenarios.", + ), + Arc::new(schema), + ) + } + fn memo_resource() -> Resource { let raw = RawResource { uri: MEMO_URI.to_string(), @@ -125,6 +187,32 @@ struct EchoArgs { env_var: Option, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +/// Scenarios for `image_scenario`, intended to exercise Codex TUI handling of MCP image outputs. +/// +/// The key behavior under test is that the TUI should render an image output cell if *any* +/// decodable image block exists in the tool result content, even if the first block is text or an +/// invalid image. +enum ImageScenario { + ImageOnly, + TextThenImage, + InvalidBase64ThenImage, + InvalidImageBytesThenImage, + MultipleValidImages, + ImageThenText, + TextOnly, +} + +#[derive(Deserialize, Debug)] +struct ImageScenarioArgs { + scenario: ImageScenario, + #[serde(default)] + caption: Option, + #[serde(default)] + data_url: Option, +} + impl ServerHandler for TestToolServer { fn get_info(&self) -> ServerInfo { ServerInfo { @@ -244,14 +332,6 @@ impl ServerHandler for TestToolServer { ) })?; - fn parse_data_url(url: &str) -> Option<(String, String)> { - let rest = url.strip_prefix("data:")?; - let (mime_and_opts, data) = rest.split_once(',')?; - let (mime, _opts) = - mime_and_opts.split_once(';').unwrap_or((mime_and_opts, "")); - Some((mime.to_string(), data.to_string())) - } - let (mime_type, data_b64) = parse_data_url(&data_url).ok_or_else(|| { McpError::invalid_params( format!("invalid data URL for image tool: {data_url}"), @@ -263,6 +343,10 @@ impl ServerHandler for TestToolServer { data_b64, mime_type, )])) } + "image_scenario" => { + let args = Self::parse_call_args::(&request, "image_scenario")?; + Self::image_scenario_result(args) + } other => Err(McpError::invalid_params( format!("unknown tool: {other}"), None, @@ -271,6 +355,89 @@ impl ServerHandler for TestToolServer { } } +impl TestToolServer { + fn parse_call_args Deserialize<'de>>( + request: &CallToolRequestParam, + tool_name: &'static str, + ) -> Result { + match request.arguments.as_ref() { + Some(arguments) => serde_json::from_value(serde_json::Value::Object( + arguments.clone().into_iter().collect(), + )) + .map_err(|err| McpError::invalid_params(err.to_string(), None)), + None => Err(McpError::invalid_params( + format!("missing arguments for {tool_name} tool"), + None, + )), + } + } + + fn image_scenario_result(args: ImageScenarioArgs) -> Result { + let (mime_type, valid_data_b64) = if let Some(data_url) = &args.data_url { + parse_data_url(data_url).ok_or_else(|| { + McpError::invalid_params( + format!("invalid data_url for image_scenario tool: {data_url}"), + None, + ) + })? + } else { + ("image/png".to_string(), SMALL_PNG_BASE64.to_string()) + }; + + let caption = args + .caption + .unwrap_or_else(|| "Here is the image:".to_string()); + + let mut content = Vec::new(); + match args.scenario { + ImageScenario::ImageOnly => { + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + } + ImageScenario::TextThenImage => { + content.push(rmcp::model::Content::text(caption)); + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + } + ImageScenario::InvalidBase64ThenImage => { + content.push(rmcp::model::Content::image( + "not-base64".to_string(), + "image/png".to_string(), + )); + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + } + ImageScenario::InvalidImageBytesThenImage => { + content.push(rmcp::model::Content::image( + "bm90IGFuIGltYWdl".to_string(), + "image/png".to_string(), + )); + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + } + ImageScenario::MultipleValidImages => { + content.push(rmcp::model::Content::image( + valid_data_b64.clone(), + mime_type.clone(), + )); + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + } + ImageScenario::ImageThenText => { + content.push(rmcp::model::Content::image(valid_data_b64, mime_type)); + content.push(rmcp::model::Content::text(caption)); + } + ImageScenario::TextOnly => { + content.push(rmcp::model::Content::text(caption)); + } + } + + Ok(CallToolResult::success(content)) + } +} + +fn parse_data_url(url: &str) -> Option<(String, String)> { + let rest = url.strip_prefix("data:")?; + let (mime_and_opts, data) = rest.split_once(',')?; + let (mime, _opts) = mime_and_opts.split_once(';').unwrap_or((mime_and_opts, "")); + Some((mime.to_string(), data.to_string())) +} + #[tokio::main] async fn main() -> Result<(), Box> { eprintln!("starting rmcp test server"); diff --git a/codex-rs/rmcp-client/src/perform_oauth_login.rs b/codex-rs/rmcp-client/src/perform_oauth_login.rs index 64cf979ec7..09b746837e 100644 --- a/codex-rs/rmcp-client/src/perform_oauth_login.rs +++ b/codex-rs/rmcp-client/src/perform_oauth_login.rs @@ -103,21 +103,32 @@ fn spawn_callback_server(server: Arc, tx: oneshot::Sender<(String, Strin tokio::task::spawn_blocking(move || { while let Ok(request) = server.recv() { let path = request.url().to_string(); - if let Some(OauthCallbackResult { code, state }) = parse_oauth_callback(&path) { - let response = - Response::from_string("Authentication complete. You may close this window."); - if let Err(err) = request.respond(response) { - eprintln!("Failed to respond to OAuth callback: {err}"); + match parse_oauth_callback(&path) { + CallbackOutcome::Success(OauthCallbackResult { code, state }) => { + let response = Response::from_string( + "Authentication complete. You may close this window.", + ); + if let Err(err) = request.respond(response) { + eprintln!("Failed to respond to OAuth callback: {err}"); + } + if let Err(err) = tx.send((code, state)) { + eprintln!("Failed to send OAuth callback: {err:?}"); + } + break; } - if let Err(err) = tx.send((code, state)) { - eprintln!("Failed to send OAuth callback: {err:?}"); + CallbackOutcome::Error(description) => { + let response = Response::from_string(format!("OAuth error: {description}")) + .with_status_code(400); + if let Err(err) = request.respond(response) { + eprintln!("Failed to respond to OAuth callback: {err}"); + } } - break; - } else { - let response = - Response::from_string("Invalid OAuth callback").with_status_code(400); - if let Err(err) = request.respond(response) { - eprintln!("Failed to respond to OAuth callback: {err}"); + CallbackOutcome::Invalid => { + let response = + Response::from_string("Invalid OAuth callback").with_status_code(400); + if let Err(err) = request.respond(response) { + eprintln!("Failed to respond to OAuth callback: {err}"); + } } } } @@ -129,29 +140,49 @@ struct OauthCallbackResult { state: String, } -fn parse_oauth_callback(path: &str) -> Option { - let (route, query) = path.split_once('?')?; +enum CallbackOutcome { + Success(OauthCallbackResult), + Error(String), + Invalid, +} + +fn parse_oauth_callback(path: &str) -> CallbackOutcome { + let Some((route, query)) = path.split_once('?') else { + return CallbackOutcome::Invalid; + }; if route != "/callback" { - return None; + return CallbackOutcome::Invalid; } let mut code = None; let mut state = None; + let mut error_description = None; for pair in query.split('&') { - let (key, value) = pair.split_once('=')?; - let decoded = decode(value).ok()?.into_owned(); + let Some((key, value)) = pair.split_once('=') else { + continue; + }; + let Ok(decoded) = decode(value) else { + continue; + }; + let decoded = decoded.into_owned(); match key { "code" => code = Some(decoded), "state" => state = Some(decoded), + "error_description" => error_description = Some(decoded), _ => {} } } - Some(OauthCallbackResult { - code: code?, - state: state?, - }) + if let (Some(code), Some(state)) = (code, state) { + return CallbackOutcome::Success(OauthCallbackResult { code, state }); + } + + if let Some(description) = error_description { + return CallbackOutcome::Error(description); + } + + CallbackOutcome::Invalid } pub struct OauthLoginHandle { diff --git a/codex-rs/rust-toolchain.toml b/codex-rs/rust-toolchain.toml index 05eeaac960..954b684895 100644 --- a/codex-rs/rust-toolchain.toml +++ b/codex-rs/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.92.0" +channel = "1.93.0" components = ["clippy", "rustfmt", "rust-src"] diff --git a/codex-rs/scripts/setup-windows.ps1 b/codex-rs/scripts/setup-windows.ps1 index 33b6b0c352..df87731380 100644 --- a/codex-rs/scripts/setup-windows.ps1 +++ b/codex-rs/scripts/setup-windows.ps1 @@ -179,7 +179,7 @@ if (-not (Ensure-Command 'cargo')) { Write-Host "==> Configuring Rust toolchain per rust-toolchain.toml" -ForegroundColor Cyan # Pin to the workspace toolchain and install components -$toolchain = '1.92.0' +$toolchain = '1.93.0' & rustup toolchain install $toolchain --profile minimal | Out-Host & rustup default $toolchain | Out-Host & rustup component add clippy rustfmt rust-src --toolchain $toolchain | Out-Host diff --git a/codex-rs/state/BUILD.bazel b/codex-rs/state/BUILD.bazel new file mode 100644 index 0000000000..b1f7932168 --- /dev/null +++ b/codex-rs/state/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "state", + crate_name = "codex_state", + compile_data = glob(["migrations/**"]), +) diff --git a/codex-rs/state/Cargo.toml b/codex-rs/state/Cargo.toml new file mode 100644 index 0000000000..810e250ca7 --- /dev/null +++ b/codex-rs/state/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "codex-state" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true } +codex-otel = { workspace = true } +codex-protocol = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sqlx = { workspace = true } +tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } +tracing = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } + +[lints] +workspace = true diff --git a/codex-rs/state/migrations/0001_threads.sql b/codex-rs/state/migrations/0001_threads.sql new file mode 100644 index 0000000000..7063ce11a4 --- /dev/null +++ b/codex-rs/state/migrations/0001_threads.sql @@ -0,0 +1,25 @@ +CREATE TABLE threads ( + id TEXT PRIMARY KEY, + rollout_path TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + source TEXT NOT NULL, + model_provider TEXT NOT NULL, + cwd TEXT NOT NULL, + title TEXT NOT NULL, + sandbox_policy TEXT NOT NULL, + approval_mode TEXT NOT NULL, + tokens_used INTEGER NOT NULL DEFAULT 0, + has_user_event INTEGER NOT NULL DEFAULT 0, + archived INTEGER NOT NULL DEFAULT 0, + archived_at INTEGER, + git_sha TEXT, + git_branch TEXT, + git_origin_url TEXT +); + +CREATE INDEX idx_threads_created_at ON threads(created_at DESC, id DESC); +CREATE INDEX idx_threads_updated_at ON threads(updated_at DESC, id DESC); +CREATE INDEX idx_threads_archived ON threads(archived); +CREATE INDEX idx_threads_source ON threads(source); +CREATE INDEX idx_threads_provider ON threads(model_provider); diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs new file mode 100644 index 0000000000..9c8ce21556 --- /dev/null +++ b/codex-rs/state/src/extract.rs @@ -0,0 +1,182 @@ +use crate::model::ThreadMetadata; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::models::is_local_image_close_tag_text; +use codex_protocol::models::is_local_image_open_tag_text; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::SessionMetaLine; +use codex_protocol::protocol::TurnContextItem; +use codex_protocol::protocol::USER_MESSAGE_BEGIN; +use serde::Serialize; +use serde_json::Value; + +/// Apply a rollout item to the metadata structure. +pub fn apply_rollout_item( + metadata: &mut ThreadMetadata, + item: &RolloutItem, + default_provider: &str, +) { + match item { + RolloutItem::SessionMeta(meta_line) => apply_session_meta_from_item(metadata, meta_line), + RolloutItem::TurnContext(turn_ctx) => apply_turn_context(metadata, turn_ctx), + RolloutItem::EventMsg(event) => apply_event_msg(metadata, event), + RolloutItem::ResponseItem(item) => apply_response_item(metadata, item), + RolloutItem::Compacted(_) => {} + } + if metadata.model_provider.is_empty() { + metadata.model_provider = default_provider.to_string(); + } +} + +fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &SessionMetaLine) { + metadata.id = meta_line.meta.id; + metadata.source = enum_to_string(&meta_line.meta.source); + if let Some(provider) = meta_line.meta.model_provider.as_deref() { + metadata.model_provider = provider.to_string(); + } + if !meta_line.meta.cwd.as_os_str().is_empty() { + metadata.cwd = meta_line.meta.cwd.clone(); + } + if let Some(git) = meta_line.git.as_ref() { + metadata.git_sha = git.commit_hash.clone(); + metadata.git_branch = git.branch.clone(); + metadata.git_origin_url = git.repository_url.clone(); + } +} + +fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) { + 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); +} + +fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) { + match event { + EventMsg::TokenCount(token_count) => { + if let Some(info) = token_count.info.as_ref() { + metadata.tokens_used = info.total_token_usage.total_tokens.max(0); + } + } + EventMsg::UserMessage(user) => { + metadata.has_user_event = true; + if metadata.title.is_empty() { + metadata.title = strip_user_message_prefix(user.message.as_str()).to_string(); + } + } + _ => {} + } +} + +fn apply_response_item(metadata: &mut ThreadMetadata, item: &ResponseItem) { + if let Some(text) = extract_user_message_text(item) { + metadata.has_user_event = true; + if metadata.title.is_empty() { + metadata.title = text; + } + } +} + +fn extract_user_message_text(item: &ResponseItem) -> Option { + let ResponseItem::Message { role, content, .. } = item else { + return None; + }; + if role != "user" { + return None; + } + let texts: Vec<&str> = content + .iter() + .filter_map(|content_item| match content_item { + ContentItem::InputText { text } => Some(text.as_str()), + ContentItem::InputImage { .. } | ContentItem::OutputText { .. } => None, + }) + .filter(|text| !is_local_image_open_tag_text(text) && !is_local_image_close_tag_text(text)) + .collect(); + if texts.is_empty() { + return None; + } + let joined = texts.join("\n"); + Some( + strip_user_message_prefix(joined.as_str()) + .trim() + .to_string(), + ) +} + +fn strip_user_message_prefix(text: &str) -> &str { + match text.find(USER_MESSAGE_BEGIN) { + Some(idx) => text[idx + USER_MESSAGE_BEGIN.len()..].trim(), + None => text.trim(), + } +} + +pub(crate) fn enum_to_string(value: &T) -> String { + match serde_json::to_value(value) { + Ok(Value::String(s)) => s, + Ok(other) => other.to_string(), + Err(_) => String::new(), + } +} + +#[cfg(test)] +mod tests { + use super::extract_user_message_text; + use crate::model::ThreadMetadata; + use chrono::DateTime; + use chrono::Utc; + use codex_protocol::ThreadId; + use codex_protocol::models::ContentItem; + use codex_protocol::models::ResponseItem; + use codex_protocol::protocol::USER_MESSAGE_BEGIN; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + use uuid::Uuid; + + #[test] + fn extracts_user_message_text() { + let item = ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ + ContentItem::InputText { + text: format!(" {USER_MESSAGE_BEGIN}actual question"), + }, + ContentItem::InputImage { + image_url: "https://example.com/image.png".to_string(), + }, + ], + end_turn: None, + }; + let actual = extract_user_message_text(&item); + assert_eq!(actual.as_deref(), Some("actual question")); + } + + #[test] + fn diff_fields_detects_changes() { + let id = ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"); + let created_at = DateTime::::from_timestamp(1_735_689_600, 0).expect("timestamp"); + let base = ThreadMetadata { + id, + rollout_path: PathBuf::from("/tmp/a.jsonl"), + created_at, + updated_at: created_at, + source: "cli".to_string(), + model_provider: "openai".to_string(), + cwd: PathBuf::from("/tmp"), + title: "hello".to_string(), + sandbox_policy: "read-only".to_string(), + approval_mode: "on-request".to_string(), + tokens_used: 1, + has_user_event: false, + archived_at: None, + git_sha: None, + git_branch: None, + git_origin_url: None, + }; + let mut other = base.clone(); + other.tokens_used = 2; + other.title = "world".to_string(); + let diffs = base.diff_fields(&other); + assert_eq!(diffs, vec!["title", "tokens_used"]); + } +} diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs new file mode 100644 index 0000000000..67533a3791 --- /dev/null +++ b/codex-rs/state/src/lib.rs @@ -0,0 +1,34 @@ +//! SQLite-backed state for rollout metadata. +//! +//! This crate is intentionally small and focused: it extracts rollout metadata +//! from JSONL rollouts and mirrors it into a local SQLite database. Backfill +//! orchestration and rollout scanning live in `codex-core`. + +mod extract; +mod migrations; +mod model; +mod paths; +mod runtime; + +/// Preferred entrypoint: owns configuration and metrics. +pub use runtime::StateRuntime; + +/// Low-level storage engine: useful for focused tests. +/// +/// Most consumers should prefer [`StateRuntime`]. +pub use extract::apply_rollout_item; +pub use model::Anchor; +pub use model::BackfillStats; +pub use model::ExtractionOutcome; +pub use model::SortKey; +pub use model::ThreadMetadata; +pub use model::ThreadMetadataBuilder; +pub use model::ThreadsPage; +pub use runtime::STATE_DB_FILENAME; + +/// Errors encountered during DB operations. Tags: [stage] +pub const DB_ERROR_METRIC: &str = "codex.db.error"; +/// Metrics on backfill process during first init of the db. Tags: [status] +pub const DB_METRIC_BACKFILL: &str = "codex.db.backfill"; +/// Metrics on errors during comparison between DB and rollout file. Tags: [stage] +pub const DB_METRIC_COMPARE_ERROR: &str = "codex.db.compare_error"; diff --git a/codex-rs/state/src/migrations.rs b/codex-rs/state/src/migrations.rs new file mode 100644 index 0000000000..24b310224b --- /dev/null +++ b/codex-rs/state/src/migrations.rs @@ -0,0 +1,3 @@ +use sqlx::migrate::Migrator; + +pub(crate) static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); diff --git a/codex-rs/state/src/model.rs b/codex-rs/state/src/model.rs new file mode 100644 index 0000000000..7d475efffc --- /dev/null +++ b/codex-rs/state/src/model.rs @@ -0,0 +1,352 @@ +use anyhow::Result; +use chrono::DateTime; +use chrono::Timelike; +use chrono::Utc; +use codex_protocol::ThreadId; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; +use sqlx::Row; +use sqlx::sqlite::SqliteRow; +use std::path::PathBuf; +use uuid::Uuid; + +/// The sort key to use when listing threads. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortKey { + /// Sort by the thread's creation timestamp. + CreatedAt, + /// Sort by the thread's last update timestamp. + UpdatedAt, +} + +/// A pagination anchor used for keyset pagination. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Anchor { + /// The timestamp component of the anchor. + pub ts: DateTime, + /// The UUID component of the anchor. + pub id: Uuid, +} + +/// A single page of thread metadata results. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThreadsPage { + /// The thread metadata items in this page. + pub items: Vec, + /// The next anchor to use for pagination, if any. + pub next_anchor: Option, + /// The number of rows scanned to produce this page. + pub num_scanned_rows: usize, +} + +/// The outcome of extracting metadata from a rollout. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtractionOutcome { + /// The extracted thread metadata. + pub metadata: ThreadMetadata, + /// The number of rollout lines that failed to parse. + pub parse_errors: usize, +} + +/// Canonical thread metadata derived from rollout files. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThreadMetadata { + /// The thread identifier. + pub id: ThreadId, + /// The absolute rollout path on disk. + pub rollout_path: PathBuf, + /// The creation timestamp. + pub created_at: DateTime, + /// The last update timestamp. + pub updated_at: DateTime, + /// The session source (stringified enum). + pub source: String, + /// The model provider identifier. + pub model_provider: String, + /// The working directory for the thread. + pub cwd: PathBuf, + /// A best-effort thread title. + pub title: String, + /// The sandbox policy (stringified enum). + pub sandbox_policy: String, + /// The approval mode (stringified enum). + pub approval_mode: String, + /// The last observed token usage. + pub tokens_used: i64, + /// Whether the thread has observed a user message. + pub has_user_event: bool, + /// The archive timestamp, if the thread is archived. + pub archived_at: Option>, + /// The git commit SHA, if known. + pub git_sha: Option, + /// The git branch name, if known. + pub git_branch: Option, + /// The git origin URL, if known. + pub git_origin_url: Option, +} + +/// Builder data required to construct [`ThreadMetadata`] without parsing filenames. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ThreadMetadataBuilder { + /// The thread identifier. + pub id: ThreadId, + /// The absolute rollout path on disk. + pub rollout_path: PathBuf, + /// The creation timestamp. + pub created_at: DateTime, + /// The last update timestamp, if known. + pub updated_at: Option>, + /// The session source. + pub source: SessionSource, + /// The model provider identifier, if known. + pub model_provider: Option, + /// The working directory for the thread. + pub cwd: PathBuf, + /// The sandbox policy. + pub sandbox_policy: SandboxPolicy, + /// The approval mode. + pub approval_mode: AskForApproval, + /// The archive timestamp, if the thread is archived. + pub archived_at: Option>, + /// The git commit SHA, if known. + pub git_sha: Option, + /// The git branch name, if known. + pub git_branch: Option, + /// The git origin URL, if known. + pub git_origin_url: Option, +} + +impl ThreadMetadataBuilder { + /// Create a new builder with required fields and sensible defaults. + pub fn new( + id: ThreadId, + rollout_path: PathBuf, + created_at: DateTime, + source: SessionSource, + ) -> Self { + Self { + id, + rollout_path, + created_at, + updated_at: None, + source, + model_provider: None, + cwd: PathBuf::new(), + sandbox_policy: SandboxPolicy::ReadOnly, + approval_mode: AskForApproval::OnRequest, + archived_at: None, + git_sha: None, + git_branch: None, + git_origin_url: None, + } + } + + /// Build canonical thread metadata, filling missing values from defaults. + pub fn build(&self, default_provider: &str) -> ThreadMetadata { + let source = crate::extract::enum_to_string(&self.source); + let sandbox_policy = crate::extract::enum_to_string(&self.sandbox_policy); + let approval_mode = crate::extract::enum_to_string(&self.approval_mode); + let created_at = canonicalize_datetime(self.created_at); + let updated_at = self + .updated_at + .map(canonicalize_datetime) + .unwrap_or(created_at); + ThreadMetadata { + id: self.id, + rollout_path: self.rollout_path.clone(), + created_at, + updated_at, + source, + model_provider: self + .model_provider + .clone() + .unwrap_or_else(|| default_provider.to_string()), + cwd: self.cwd.clone(), + title: String::new(), + sandbox_policy, + approval_mode, + tokens_used: 0, + has_user_event: false, + archived_at: self.archived_at.map(canonicalize_datetime), + git_sha: self.git_sha.clone(), + git_branch: self.git_branch.clone(), + git_origin_url: self.git_origin_url.clone(), + } + } +} + +impl ThreadMetadata { + /// Return the list of field names that differ between `self` and `other`. + pub fn diff_fields(&self, other: &Self) -> Vec<&'static str> { + let mut diffs = Vec::new(); + if self.id != other.id { + diffs.push("id"); + } + if self.rollout_path != other.rollout_path { + diffs.push("rollout_path"); + } + if self.created_at != other.created_at { + diffs.push("created_at"); + } + if self.updated_at != other.updated_at { + diffs.push("updated_at"); + } + if self.source != other.source { + diffs.push("source"); + } + if self.model_provider != other.model_provider { + diffs.push("model_provider"); + } + if self.cwd != other.cwd { + diffs.push("cwd"); + } + if self.title != other.title { + diffs.push("title"); + } + if self.sandbox_policy != other.sandbox_policy { + diffs.push("sandbox_policy"); + } + if self.approval_mode != other.approval_mode { + diffs.push("approval_mode"); + } + if self.tokens_used != other.tokens_used { + diffs.push("tokens_used"); + } + if self.has_user_event != other.has_user_event { + diffs.push("has_user_event"); + } + if self.archived_at != other.archived_at { + diffs.push("archived_at"); + } + if self.git_sha != other.git_sha { + diffs.push("git_sha"); + } + if self.git_branch != other.git_branch { + diffs.push("git_branch"); + } + if self.git_origin_url != other.git_origin_url { + diffs.push("git_origin_url"); + } + diffs + } +} + +fn canonicalize_datetime(dt: DateTime) -> DateTime { + dt.with_nanosecond(0).unwrap_or(dt) +} + +#[derive(Debug)] +pub(crate) struct ThreadRow { + id: String, + rollout_path: String, + created_at: i64, + updated_at: i64, + source: String, + model_provider: String, + cwd: String, + title: String, + sandbox_policy: String, + approval_mode: String, + tokens_used: i64, + has_user_event: bool, + archived_at: Option, + git_sha: Option, + git_branch: Option, + git_origin_url: Option, +} + +impl ThreadRow { + pub(crate) fn try_from_row(row: &SqliteRow) -> Result { + Ok(Self { + id: row.try_get("id")?, + rollout_path: row.try_get("rollout_path")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + source: row.try_get("source")?, + model_provider: row.try_get("model_provider")?, + cwd: row.try_get("cwd")?, + title: row.try_get("title")?, + sandbox_policy: row.try_get("sandbox_policy")?, + approval_mode: row.try_get("approval_mode")?, + tokens_used: row.try_get("tokens_used")?, + has_user_event: row.try_get("has_user_event")?, + archived_at: row.try_get("archived_at")?, + git_sha: row.try_get("git_sha")?, + git_branch: row.try_get("git_branch")?, + git_origin_url: row.try_get("git_origin_url")?, + }) + } +} + +impl TryFrom for ThreadMetadata { + type Error = anyhow::Error; + + fn try_from(row: ThreadRow) -> std::result::Result { + let ThreadRow { + id, + rollout_path, + created_at, + updated_at, + source, + model_provider, + cwd, + title, + sandbox_policy, + approval_mode, + tokens_used, + has_user_event, + archived_at, + git_sha, + git_branch, + git_origin_url, + } = row; + Ok(Self { + id: ThreadId::try_from(id)?, + rollout_path: PathBuf::from(rollout_path), + created_at: epoch_seconds_to_datetime(created_at)?, + updated_at: epoch_seconds_to_datetime(updated_at)?, + source, + model_provider, + cwd: PathBuf::from(cwd), + title, + sandbox_policy, + approval_mode, + tokens_used, + has_user_event, + archived_at: archived_at.map(epoch_seconds_to_datetime).transpose()?, + git_sha, + git_branch, + git_origin_url, + }) + } +} + +pub(crate) fn anchor_from_item(item: &ThreadMetadata, sort_key: SortKey) -> Option { + let id = Uuid::parse_str(&item.id.to_string()).ok()?; + let ts = match sort_key { + SortKey::CreatedAt => item.created_at, + SortKey::UpdatedAt => item.updated_at, + }; + Some(Anchor { ts, id }) +} + +pub(crate) fn datetime_to_epoch_seconds(dt: DateTime) -> i64 { + dt.timestamp() +} + +pub(crate) fn epoch_seconds_to_datetime(secs: i64) -> Result> { + DateTime::::from_timestamp(secs, 0) + .ok_or_else(|| anyhow::anyhow!("invalid unix timestamp: {secs}")) +} + +/// Statistics about a backfill operation. +#[derive(Debug, Clone)] +pub struct BackfillStats { + /// The number of rollout files scanned. + pub scanned: usize, + /// The number of rows upserted successfully. + pub upserted: usize, + /// The number of rows that failed to upsert. + pub failed: usize, +} diff --git a/codex-rs/state/src/paths.rs b/codex-rs/state/src/paths.rs new file mode 100644 index 0000000000..8123743821 --- /dev/null +++ b/codex-rs/state/src/paths.rs @@ -0,0 +1,10 @@ +use chrono::DateTime; +use chrono::Timelike; +use chrono::Utc; +use std::path::Path; + +pub(crate) async fn file_modified_time_utc(path: &Path) -> Option> { + let modified = tokio::fs::metadata(path).await.ok()?.modified().ok()?; + let updated_at: DateTime = modified.into(); + Some(updated_at.with_nanosecond(0).unwrap_or(updated_at)) +} diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs new file mode 100644 index 0000000000..f341ee3bb6 --- /dev/null +++ b/codex-rs/state/src/runtime.rs @@ -0,0 +1,458 @@ +use crate::DB_ERROR_METRIC; +use crate::SortKey; +use crate::ThreadMetadata; +use crate::ThreadMetadataBuilder; +use crate::ThreadsPage; +use crate::apply_rollout_item; +use crate::migrations::MIGRATOR; +use crate::model::ThreadRow; +use crate::model::anchor_from_item; +use crate::model::datetime_to_epoch_seconds; +use crate::paths::file_modified_time_utc; +use chrono::DateTime; +use chrono::Utc; +use codex_otel::OtelManager; +use codex_protocol::ThreadId; +use codex_protocol::protocol::RolloutItem; +use sqlx::QueryBuilder; +use sqlx::Row; +use sqlx::Sqlite; +use sqlx::SqlitePool; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::sqlite::SqliteJournalMode; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::sqlite::SqliteSynchronous; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use tracing::warn; + +pub const STATE_DB_FILENAME: &str = "state.sqlite"; + +const METRIC_DB_INIT: &str = "codex.db.init"; + +#[derive(Clone)] +pub struct StateRuntime { + codex_home: PathBuf, + default_provider: String, + pool: Arc, +} + +impl StateRuntime { + /// Initialize the state runtime using the provided Codex home and default provider. + /// + /// This opens (and migrates) the SQLite database at `codex_home/state.sqlite`. + pub async fn init( + codex_home: PathBuf, + default_provider: String, + otel: Option, + ) -> anyhow::Result> { + tokio::fs::create_dir_all(&codex_home).await?; + let state_path = codex_home.join(STATE_DB_FILENAME); + let existed = tokio::fs::try_exists(&state_path).await.unwrap_or(false); + let pool = match open_sqlite(&state_path).await { + Ok(db) => Arc::new(db), + Err(err) => { + warn!("failed to open state db at {}: {err}", state_path.display()); + if let Some(otel) = otel.as_ref() { + otel.counter(METRIC_DB_INIT, 1, &[("status", "open_error")]); + } + return Err(err); + } + }; + if let Some(otel) = otel.as_ref() { + otel.counter(METRIC_DB_INIT, 1, &[("status", "opened")]); + } + let runtime = Arc::new(Self { + pool, + codex_home, + default_provider, + }); + if !existed && let Some(otel) = otel.as_ref() { + otel.counter(METRIC_DB_INIT, 1, &[("status", "created")]); + } + Ok(runtime) + } + + /// Return the configured Codex home directory for this runtime. + pub fn codex_home(&self) -> &Path { + self.codex_home.as_path() + } + + /// 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, + model_provider, + cwd, + title, + sandbox_policy, + approval_mode, + tokens_used, + has_user_event, + 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() + } + + /// 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, + model_provider, + cwd, + title, + sandbox_policy, + approval_mode, + tokens_used, + has_user_event, + 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, + }) + } + + /// 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, + model_provider, + cwd, + title, + sandbox_policy, + approval_mode, + tokens_used, + has_user_event, + 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, + model_provider = excluded.model_provider, + cwd = excluded.cwd, + title = excluded.title, + sandbox_policy = excluded.sandbox_policy, + approval_mode = excluded.approval_mode, + tokens_used = excluded.tokens_used, + has_user_event = excluded.has_user_event, + 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.model_provider.as_str()) + .bind(metadata.cwd.display().to_string()) + .bind(metadata.title.as_str()) + .bind(metadata.sandbox_policy.as_str()) + .bind(metadata.approval_mode.as_str()) + .bind(metadata.tokens_used) + .bind(metadata.has_user_event) + .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(()) + } + + /// 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; + } + 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); + } + 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 + } +} + +async fn open_sqlite(path: &Path) -> anyhow::Result { + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .busy_timeout(Duration::from_secs(5)); + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(options) + .await?; + MIGRATOR.run(&pool).await?; + Ok(pool) +} + +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 has_user_event = 1"); + 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); +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5413fb1496..e5db8fc6b6 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -42,7 +42,6 @@ use codex_core::config::ConfigOverrides; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; -#[cfg(target_os = "windows")] use codex_core::features::Feature; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; @@ -58,9 +57,13 @@ use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionSource; use codex_core::protocol::SkillErrorInfo; use codex_core::protocol::TokenUsage; +#[cfg(target_os = "windows")] +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; +#[cfg(target_os = "windows")] +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::items::TurnItem; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; @@ -920,6 +923,7 @@ impl App { let app_event_tx = AppEventSender::new(app_event_tx); emit_deprecation_notice(&app_event_tx, ollama_chat_support_notice); emit_project_config_warnings(&app_event_tx, &config); + tui.set_notification_method(config.tui_notification_method); let harness_overrides = normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?; @@ -1088,7 +1092,8 @@ impl App { // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. #[cfg(target_os = "windows")] { - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = WindowsSandboxLevel::from_config(&app.config) + != WindowsSandboxLevel::Disabled && matches!( app.config.sandbox_policy.get(), codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } @@ -1332,6 +1337,7 @@ impl App { Ok(resumed) => { self.shutdown_current_thread().await; self.config = resume_config; + tui.set_notification_method(self.config.tui_notification_method); self.file_search = FileSearchManager::new( self.config.cwd.clone(), self.app_event_tx.clone(), @@ -1503,9 +1509,7 @@ impl App { tui.frame_requester().schedule_frame(); } AppEvent::StartFileSearch(query) => { - if !query.is_empty() { - self.file_search.on_user_query(query); - } + self.file_search.on_user_query(query); } AppEvent::FileSearchResult { query, matches } => { self.chat_widget.apply_file_search_result(query, matches); @@ -1628,10 +1632,27 @@ impl App { } } Err(err) => { + let mut code_tag: Option = None; + let mut message_tag: Option = None; + if let Some((code, message)) = + codex_core::windows_sandbox::elevated_setup_failure_details( + &err, + ) + { + code_tag = Some(code); + message_tag = Some(message); + } + let mut tags: Vec<(&str, &str)> = Vec::new(); + if let Some(code) = code_tag.as_deref() { + tags.push(("code", code)); + } + if let Some(message) = message_tag.as_deref() { + tags.push(("message", message)); + } otel_manager.counter( "codex.windows_sandbox.elevated_setup_failure", 1, - &[], + &tags, ); tracing::error!( error = %err, @@ -1666,27 +1687,48 @@ impl App { let feature_key = Feature::WindowsSandbox.key(); let elevated_key = Feature::WindowsSandboxElevated.key(); let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated); - match ConfigEditsBuilder::new(&self.config.codex_home) - .with_profile(profile) - .set_feature_enabled(feature_key, true) - .set_feature_enabled(elevated_key, elevated_enabled) - .apply() - .await - { + let mut builder = + ConfigEditsBuilder::new(&self.config.codex_home).with_profile(profile); + if elevated_enabled { + builder = builder.set_feature_enabled(elevated_key, true); + } else { + builder = builder + .set_feature_enabled(feature_key, true) + .set_feature_enabled(elevated_key, false); + } + match builder.apply().await { Ok(()) => { - self.config.set_windows_sandbox_globally(true); - self.config - .set_windows_elevated_sandbox_globally(elevated_enabled); - self.chat_widget - .set_feature_enabled(Feature::WindowsSandbox, true); - self.chat_widget.set_feature_enabled( - Feature::WindowsSandboxElevated, - elevated_enabled, - ); + if elevated_enabled { + self.config.set_windows_elevated_sandbox_enabled(true); + self.chat_widget + .set_feature_enabled(Feature::WindowsSandboxElevated, true); + } else { + self.config.set_windows_sandbox_enabled(true); + self.config.set_windows_elevated_sandbox_enabled(false); + self.chat_widget + .set_feature_enabled(Feature::WindowsSandbox, true); + self.chat_widget + .set_feature_enabled(Feature::WindowsSandboxElevated, false); + } self.chat_widget.clear_forced_auto_mode_downgrade(); + let windows_sandbox_level = + WindowsSandboxLevel::from_config(&self.config); if let Some((sample_paths, extra_count, failed_scan)) = self.chat_widget.world_writable_warning_details() { + self.app_event_tx.send(AppEvent::CodexOp( + Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + windows_sandbox_level: Some(windows_sandbox_level), + model: None, + effort: None, + summary: None, + collaboration_mode: None, + personality: None, + }, + )); self.app_event_tx.send( AppEvent::OpenWorldWritableWarningConfirmation { preset: Some(preset.clone()), @@ -1701,6 +1743,7 @@ impl App { cwd: None, approval_policy: Some(preset.approval), sandbox_policy: Some(preset.sandbox.clone()), + windows_sandbox_level: Some(windows_sandbox_level), model: None, effort: None, summary: None, @@ -1839,7 +1882,8 @@ impl App { } #[cfg(target_os = "windows")] if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some() + || WindowsSandboxLevel::from_config(&self.config) + != WindowsSandboxLevel::Disabled { self.config.forced_auto_mode_downgraded_on_windows = false; } @@ -1861,7 +1905,8 @@ impl App { return Ok(AppRunControl::Continue); } - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = WindowsSandboxLevel::from_config(&self.config) + != WindowsSandboxLevel::Disabled && policy_is_workspace_write_or_ro && !self.chat_widget.world_writable_warning_hidden(); if should_check { @@ -1885,6 +1930,12 @@ impl App { if updates.is_empty() { return Ok(AppRunControl::Continue); } + let windows_sandbox_changed = updates.iter().any(|(feature, _)| { + matches!( + feature, + Feature::WindowsSandbox | Feature::WindowsSandboxElevated + ) + }); let mut builder = ConfigEditsBuilder::new(&self.config.codex_home) .with_profile(self.active_profile.as_deref()); for (feature, enabled) in &updates { @@ -1910,6 +1961,24 @@ impl App { } } } + if windows_sandbox_changed { + #[cfg(target_os = "windows")] + { + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + self.app_event_tx + .send(AppEvent::CodexOp(Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + windows_sandbox_level: Some(windows_sandbox_level), + model: None, + effort: None, + summary: None, + collaboration_mode: None, + personality: None, + })); + } + } if let Err(err) = builder.apply().await { tracing::error!(error = %err, "failed to persist feature flags"); self.chat_widget.add_error_message(format!( @@ -2222,8 +2291,9 @@ impl App { Err(external_editor::EditorError::MissingEditor) => { self.chat_widget .add_to_history(history_cell::new_error_event( - "Cannot open external editor: set $VISUAL or $EDITOR".to_string(), - )); + "Cannot open external editor: set $VISUAL or $EDITOR before starting Codex." + .to_string(), + )); self.reset_external_editor_state(tui); return; } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index f1ec28f3f1..b65d1efb62 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -178,6 +178,9 @@ pub(crate) enum AppEvent { mode: WindowsSandboxEnableMode, }, + /// Update the Windows sandbox feature mode without changing approval presets. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + /// Update the current approval policy in the running app and widget. UpdateAskForApprovalPolicy(AskForApproval), diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index d143449226..039a9ca051 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -21,6 +21,12 @@ pub(crate) trait BottomPaneView: Renderable { CancellationEvent::NotHandled } + /// Return true if Esc should be routed through `handle_key_event` instead + /// of the `on_ctrl_c` cancellation path. + fn prefer_esc_to_handle_key_event(&self) -> bool { + false + } + /// Optional paste handler. Return true if the view modified its state and /// needs a redraw. fn handle_paste(&mut self, _pasted: String) -> bool { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index c283b10ab1..3a3f5d407b 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -15,6 +15,16 @@ //! [`ChatComposer::handle_key_event_without_popup`]. After every handled key, we call //! [`ChatComposer::sync_popups`] so UI state follows the latest buffer/cursor. //! +//! # History Navigation (↑/↓) +//! +//! The Up/Down history path is managed by [`ChatComposerHistory`]. It merges: +//! +//! - Persistent cross-session history (text-only; no element ranges or attachments). +//! - Local in-session history (full text + text elements + local image paths). +//! +//! When recalling a local entry, the composer rehydrates text elements and image attachments. +//! When recalling a persistent entry, only the text is restored. +//! //! # Submission and Prompt Expansion //! //! On submit/queue paths, the composer: @@ -89,6 +99,7 @@ use ratatui::widgets::StatefulWidgetRef; use ratatui::widgets::WidgetRef; use super::chat_composer_history::ChatComposerHistory; +use super::chat_composer_history::HistoryEntry; use super::command_popup::CommandItem; use super::command_popup::CommandPopup; use super::command_popup::CommandPopupFlags; @@ -96,15 +107,20 @@ use super::file_search_popup::FileSearchPopup; use super::footer::CollaborationModeIndicator; use super::footer::FooterMode; use super::footer::FooterProps; +use super::footer::SummaryLeft; +use super::footer::can_show_left_with_context; +use super::footer::context_window_line; use super::footer::esc_hint_mode; use super::footer::footer_height; use super::footer::footer_hint_items_width; use super::footer::footer_line_width; use super::footer::inset_footer_hint_area; -use super::footer::render_footer; +use super::footer::render_context_right; +use super::footer::render_footer_from_props; use super::footer::render_footer_hint_items; -use super::footer::render_mode_indicator; +use super::footer::render_footer_line; use super::footer::reset_mode_after_activity; +use super::footer::single_line_footer_layout; use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; @@ -227,7 +243,6 @@ impl ChatComposerConfig { } } } - pub(crate) struct ChatComposer { textarea: TextArea, textarea_state: RefCell, @@ -268,6 +283,7 @@ pub(crate) struct ChatComposer { config: ChatComposerConfig, collaboration_mode_indicator: Option, personality_command_enabled: bool, + windows_degraded_sandbox_active: bool, } #[derive(Clone, Debug)] @@ -341,7 +357,7 @@ impl ChatComposer { paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), - footer_mode: FooterMode::ShortcutSummary, + footer_mode: FooterMode::ComposerEmpty, footer_hint_override: None, footer_flash: None, context_window_percent: None, @@ -353,6 +369,7 @@ impl ChatComposer { config, collaboration_mode_indicator: None, personality_command_enabled: false, + windows_degraded_sandbox_active: false, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -387,7 +404,6 @@ impl ChatComposer { pub fn set_personality_command_enabled(&mut self, enabled: bool) { self.personality_command_enabled = enabled; } - /// Centralized feature gating keeps config checks out of call sites. fn popups_enabled(&self) -> bool { self.config.popups_enabled @@ -400,6 +416,10 @@ impl ChatComposer { fn image_paste_enabled(&self) -> bool { self.config.image_paste_enabled } + #[cfg(target_os = "windows")] + pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) { + self.windows_degraded_sandbox_active = enabled; + } fn layout_areas(&self, area: Rect) -> [Rect; 3] { let footer_props = self.footer_props(); let footer_hint_height = self @@ -451,11 +471,12 @@ impl ChatComposer { offset: usize, entry: Option, ) -> bool { - let Some(text) = self.history.on_entry_response(log_id, offset, entry) else { + let Some(entry) = self.history.on_entry_response(log_id, offset, entry) else { return false; }; - // Composer history (↑/↓) stores plain text only; no UI element ranges/attachments to restore here. - self.set_text_content(text, Vec::new(), Vec::new()); + // Persistent ↑/↓ history is text-only (backwards-compatible and avoids persisting + // attachments), but local in-session ↑/↓ history can rehydrate elements and image paths. + self.set_text_content(entry.text, entry.text_elements, entry.local_image_paths); true } @@ -632,6 +653,18 @@ impl ChatComposer { text } + pub(crate) fn pending_pastes(&self) -> Vec<(String, String)> { + self.pending_pastes.clone() + } + + pub(crate) fn set_pending_pastes(&mut self, pending_pastes: Vec<(String, String)>) { + let text = self.textarea.text().to_string(); + self.pending_pastes = pending_pastes + .into_iter() + .filter(|(placeholder, _)| text.contains(placeholder)) + .collect(); + } + /// Override the footer hint items displayed beneath the composer. Passing /// `None` restores the default shortcut footer. pub(crate) fn set_footer_hint_override(&mut self, items: Option>) { @@ -698,9 +731,19 @@ impl ChatComposer { return None; } let previous = self.current_text(); + let text_elements = self.textarea.text_elements(); + let local_image_paths = self + .attached_images + .iter() + .map(|img| img.path.clone()) + .collect(); self.set_text_content(String::new(), Vec::new(), Vec::new()); self.history.reset_navigation(); - self.history.record_local_submission(&previous); + self.history.record_local_submission(HistoryEntry { + text: previous.clone(), + text_elements, + local_image_paths, + }); Some(previous) } @@ -1400,7 +1443,7 @@ impl ChatComposer { } /// Expand large-paste placeholders using element ranges and rebuild other element spans. - fn expand_pending_pastes( + pub(crate) fn expand_pending_pastes( text: &str, mut elements: Vec, pending_pastes: &[(String, String)], @@ -1723,6 +1766,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) .is_some(); let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:"); @@ -1782,8 +1826,17 @@ impl ChatComposer { if text.is_empty() && self.attached_images.is_empty() { return None; } - if !text.is_empty() { - self.history.record_local_submission(&text); + if !text.is_empty() || !self.attached_images.is_empty() { + let local_image_paths = self + .attached_images + .iter() + .map(|img| img.path.clone()) + .collect(); + self.history.record_local_submission(HistoryEntry { + text: text.clone(), + text_elements: text_elements.clone(), + local_image_paths, + }); } self.pending_pastes.clear(); Some((text, text_elements)) @@ -1899,6 +1952,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) { self.textarea.set_text_clearing_elements(""); @@ -1926,6 +1980,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) && cmd == SlashCommand::Review { @@ -1977,15 +2032,19 @@ impl ChatComposer { .history .should_handle_navigation(self.textarea.text(), self.textarea.cursor()) { - let replace_text = match key_event.code { + let replace_entry = match key_event.code { KeyCode::Up => self.history.navigate_up(&self.app_event_tx), KeyCode::Down => self.history.navigate_down(&self.app_event_tx), KeyCode::Char('p') => self.history.navigate_up(&self.app_event_tx), KeyCode::Char('n') => self.history.navigate_down(&self.app_event_tx), _ => unreachable!(), }; - if let Some(text) = replace_text { - self.set_text_content(text, Vec::new(), Vec::new()); + if let Some(entry) = replace_entry { + self.set_text_content( + entry.text, + entry.text_elements, + entry.local_image_paths, + ); return (InputResult::None, true); } } @@ -2234,39 +2293,69 @@ impl ChatComposer { return false; } - let next = toggle_shortcut_mode(self.footer_mode, self.quit_shortcut_hint_visible()); + let next = toggle_shortcut_mode( + self.footer_mode, + self.quit_shortcut_hint_visible(), + self.is_empty(), + ); let changed = next != self.footer_mode; self.footer_mode = next; changed } fn footer_props(&self) -> FooterProps { + let mode = self.footer_mode(); + let is_wsl = { + #[cfg(target_os = "linux")] + { + mode == FooterMode::ShortcutOverlay && crate::clipboard_paste::is_probably_wsl() + } + #[cfg(not(target_os = "linux"))] + { + false + } + }; + FooterProps { - mode: self.footer_mode(), + mode, esc_backtrack_hint: self.esc_backtrack_hint, 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, context_window_used_tokens: self.context_window_used_tokens, } } + /// Resolve the effective footer mode via a small priority waterfall. + /// + /// The base mode is derived solely from whether the composer is empty: + /// `ComposerEmpty` iff empty, otherwise `ComposerHasDraft`. Transient + /// modes (Esc hint, overlay, quit reminder) can override that base when + /// their conditions are active. fn footer_mode(&self) -> FooterMode { + let base_mode = if self.is_empty() { + FooterMode::ComposerEmpty + } else { + FooterMode::ComposerHasDraft + }; + match self.footer_mode { FooterMode::EscHint => FooterMode::EscHint, FooterMode::ShortcutOverlay => FooterMode::ShortcutOverlay, FooterMode::QuitShortcutReminder if self.quit_shortcut_hint_visible() => { FooterMode::QuitShortcutReminder } - FooterMode::QuitShortcutReminder => FooterMode::ShortcutSummary, - FooterMode::ShortcutSummary if self.quit_shortcut_hint_visible() => { + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft + if self.quit_shortcut_hint_visible() => + { FooterMode::QuitShortcutReminder } - FooterMode::ShortcutSummary if !self.is_empty() => FooterMode::ContextOnly, - other => other, + FooterMode::QuitShortcutReminder => base_mode, + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => base_mode, } } @@ -2291,6 +2380,11 @@ impl ChatComposer { // When browsing input history (shell-style Up/Down recall), skip all popup // synchronization so nothing steals focus from continued history navigation. if browsing_history { + if self.current_file_query.is_some() { + self.app_event_tx + .send(AppEvent::StartFileSearch(String::new())); + self.current_file_query = None; + } self.active_popup = ActivePopup::None; return; } @@ -2301,12 +2395,22 @@ impl ChatComposer { self.sync_command_popup(allow_command_popup); if matches!(self.active_popup, ActivePopup::Command(_)) { + if self.current_file_query.is_some() { + self.app_event_tx + .send(AppEvent::StartFileSearch(String::new())); + self.current_file_query = None; + } self.dismissed_file_popup_token = None; self.dismissed_skill_popup_token = None; return; } if let Some(token) = skill_token { + if self.current_file_query.is_some() { + self.app_event_tx + .send(AppEvent::StartFileSearch(String::new())); + self.current_file_query = None; + } self.sync_skill_popup(token); return; } @@ -2317,6 +2421,11 @@ impl ChatComposer { return; } + if self.current_file_query.is_some() { + self.app_event_tx + .send(AppEvent::StartFileSearch(String::new())); + self.current_file_query = None; + } self.dismissed_file_popup_token = None; if matches!( self.active_popup, @@ -2369,6 +2478,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) { return true; } @@ -2425,6 +2535,7 @@ impl ChatComposer { CommandPopupFlags { collaboration_modes_enabled, personality_command_enabled, + windows_degraded_sandbox_active: self.windows_degraded_sandbox_active, }, ); command_popup.on_composer_text_change(first_line.to_string()); @@ -2448,7 +2559,10 @@ impl ChatComposer { return; } - if !query.is_empty() { + if query.is_empty() { + self.app_event_tx + .send(AppEvent::StartFileSearch(String::new())); + } else { self.app_event_tx .send(AppEvent::StartFileSearch(query.clone())); } @@ -2472,7 +2586,11 @@ impl ChatComposer { } } - self.current_file_query = Some(query); + if query.is_empty() { + self.current_file_query = None; + } else { + self.current_file_query = Some(query); + } self.dismissed_file_popup_token = None; } @@ -2584,6 +2702,28 @@ impl Renderable for ChatComposer { } ActivePopup::None => { let footer_props = self.footer_props(); + let show_cycle_hint = !footer_props.is_task_running; + let show_shortcuts_hint = match footer_props.mode { + FooterMode::ComposerEmpty => !self.is_in_paste_burst(), + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match footer_props.mode { + FooterMode::ComposerHasDraft => { + footer_props.is_task_running && footer_props.steer_enabled + } + FooterMode::QuitShortcutReminder + | FooterMode::ComposerEmpty + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; + let context_line = context_window_line( + footer_props.context_window_percent, + footer_props.context_window_used_tokens, + ); + let context_width = context_line.width() as u16; let custom_height = self.custom_footer_height(); let footer_hint_height = custom_height.unwrap_or_else(|| footer_height(footer_props)); @@ -2598,26 +2738,102 @@ impl Renderable for ChatComposer { } else { popup_rect }; - let mut left_content_width = None; - if self.footer_flash_visible() { + let left_width = if self.footer_flash_visible() { + self.footer_flash + .as_ref() + .map(|flash| flash.line.width() as u16) + .unwrap_or(0) + } else if let Some(items) = self.footer_hint_override.as_ref() { + footer_hint_items_width(items) + } else { + footer_line_width( + footer_props, + self.collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ) + }; + let can_show_left_and_context = + can_show_left_with_context(hint_rect, left_width, context_width); + let has_override = + self.footer_flash_visible() || self.footer_hint_override.is_some(); + let single_line_layout = if has_override { + None + } else { + match footer_props.mode { + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => { + // Both of these modes render the single-line footer style (with + // either the shortcuts hint or the optional queue hint). We still + // want the single-line collapse rules so the mode label can win over + // the context indicator on narrow widths. + Some(single_line_footer_layout( + hint_rect, + context_width, + self.collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + )) + } + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay => None, + } + }; + let show_context = if matches!( + footer_props.mode, + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + ) { + false + } else { + single_line_layout + .as_ref() + .map(|(_, show_context)| *show_context) + .unwrap_or(can_show_left_and_context) + }; + + if let Some((summary_left, _)) = single_line_layout { + match summary_left { + SummaryLeft::Default => { + render_footer_from_props( + hint_rect, + buf, + footer_props, + self.collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + } + SummaryLeft::Custom(line) => { + render_footer_line(hint_rect, buf, line); + } + SummaryLeft::None => {} + } + } else if self.footer_flash_visible() { if let Some(flash) = self.footer_flash.as_ref() { flash.line.render(inset_footer_hint_area(hint_rect), buf); - left_content_width = Some(flash.line.width() as u16); } } else if let Some(items) = self.footer_hint_override.as_ref() { render_footer_hint_items(hint_rect, buf, items); - left_content_width = Some(footer_hint_items_width(items)); } else { - render_footer(hint_rect, buf, footer_props); - left_content_width = Some(footer_line_width(footer_props)); + render_footer_from_props( + hint_rect, + buf, + footer_props, + self.collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + } + + if show_context { + render_context_right(hint_rect, buf, &context_line); } - render_mode_indicator( - hint_rect, - buf, - self.collaboration_mode_indicator, - !footer_props.is_task_running, - left_content_width, - ); } } let style = user_message_style(); @@ -2647,8 +2863,11 @@ impl Renderable for ChatComposer { .unwrap_or("Input disabled.") .to_string() }; - let placeholder = Span::from(text).dim(); - Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); + if !textarea_rect.is_empty() { + let placeholder = Span::from(text).dim(); + Line::from(vec![placeholder]) + .render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); + } } } } @@ -2861,14 +3080,17 @@ mod tests { ); } - fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) - where + fn snapshot_composer_state_with_width( + name: &str, + width: u16, + enhanced_keys_supported: bool, + setup: F, + ) where F: FnOnce(&mut ChatComposer), { use ratatui::Terminal; use ratatui::backend::TestBackend; - let width = 100; let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); let mut composer = ChatComposer::new( @@ -2890,6 +3112,13 @@ mod tests { insta::assert_snapshot!(name, terminal.backend()); } + fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) + where + F: FnOnce(&mut ChatComposer), + { + snapshot_composer_state_with_width(name, 100, enhanced_keys_supported, setup); + } + #[test] fn footer_mode_snapshots() { use crossterm::event::KeyCode; @@ -2942,6 +3171,100 @@ mod tests { }); } + #[test] + fn footer_collapse_snapshots() { + fn setup_collab_footer( + composer: &mut ChatComposer, + context_percent: i64, + indicator: CollaborationModeIndicator, + ) { + composer.set_collaboration_modes_enabled(true); + composer.set_collaboration_mode_indicator(Some(indicator)); + composer.set_context_window(Some(context_percent), None); + } + + // Empty textarea, agent idle: shortcuts hint can show, and cycle hint is available. + snapshot_composer_state_with_width("footer_collapse_empty_full", 120, true, |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_cycle_with_context", + 60, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_cycle_without_context", + 44, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_only", + 26, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + + // Textarea has content, agent running, steer enabled: queue hint is shown. + snapshot_composer_state_with_width("footer_collapse_queue_full", 120, true, |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }); + snapshot_composer_state_with_width( + "footer_collapse_queue_short_with_context", + 50, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_message_without_context", + 40, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_short_without_context", + 30, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_mode_only", + 20, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + } + #[test] fn esc_hint_stays_hidden_with_draft_content() { use crossterm::event::KeyCode; @@ -2962,15 +3285,40 @@ mod tests { assert!(!composer.is_empty()); assert_eq!(composer.current_text(), "d"); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); assert!(matches!(composer.active_popup, ActivePopup::None)); let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); assert!(!composer.esc_backtrack_hint); } + #[test] + fn base_footer_mode_tracks_empty_state_after_quit_hint_expires() { + use crossterm::event::KeyCode; + + 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, + ); + + type_chars_humanlike(&mut composer, &['d']); + composer.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c')), true); + composer.quit_shortcut_expires_at = + Some(Instant::now() - std::time::Duration::from_secs(1)); + + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); + + composer.set_text_content(String::new(), Vec::new(), Vec::new()); + assert_eq!(composer.footer_mode(), FooterMode::ComposerEmpty); + } + #[test] fn clear_for_ctrl_c_records_cleared_draft() { let (tx, _rx) = unbounded_channel::(); @@ -2994,7 +3342,7 @@ mod tests { assert_eq!( composer.history.navigate_up(&composer.app_event_tx), - Some("draft text".to_string()) + Some(HistoryEntry::from_text("draft text".to_string())) ); } @@ -3031,11 +3379,11 @@ mod tests { // Toggle back to prompt mode so subsequent typing captures characters. let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); type_chars_humanlike(&mut composer, &['h']); assert_eq!(composer.textarea.text(), "h"); - assert_eq!(composer.footer_mode(), FooterMode::ContextOnly); + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); let (result, needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); @@ -3043,8 +3391,8 @@ mod tests { assert!(needs_redraw, "typing should still mark the view dirty"); let _ = flush_after_paste_burst(&mut composer); assert_eq!(composer.textarea.text(), "h?"); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); - assert_eq!(composer.footer_mode(), FooterMode::ContextOnly); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); } /// Behavior: while a paste-like burst is being captured, `?` must not toggle the shortcut @@ -4447,6 +4795,37 @@ mod tests { assert_eq!(vec![path], imgs); } + #[test] + fn history_navigation_restores_image_attachments() { + 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 path = PathBuf::from("/tmp/image1.png"); + composer.attach_image(path.clone()); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert!(matches!(result, InputResult::Submitted { .. })); + + composer.set_text_content(String::new(), Vec::new(), Vec::new()); + + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + + let text = composer.current_text(); + assert_eq!(text, "[Image #1]"); + let text_elements = composer.text_elements(); + assert_eq!(text_elements.len(), 1); + assert_eq!(text_elements[0].placeholder(&text), Some("[Image #1]")); + assert_eq!(composer.local_image_paths(), vec![path]); + } + #[test] fn set_text_content_reattaches_images_without_placeholder_metadata() { let (tx, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index 991283a566..da9f46ae4b 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -1,8 +1,35 @@ use std::collections::HashMap; +use std::path::PathBuf; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use codex_core::protocol::Op; +use codex_protocol::user_input::TextElement; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HistoryEntry { + pub(crate) text: String, + pub(crate) text_elements: Vec, + pub(crate) local_image_paths: Vec, +} + +impl HistoryEntry { + fn empty() -> Self { + Self { + text: String::new(), + text_elements: Vec::new(), + local_image_paths: Vec::new(), + } + } + + pub(crate) fn from_text(text: String) -> Self { + Self { + text, + text_elements: Vec::new(), + local_image_paths: Vec::new(), + } + } +} /// State machine that manages shell-style history navigation (Up/Down) inside /// the chat composer. This struct is intentionally decoupled from the @@ -15,10 +42,10 @@ pub(crate) struct ChatComposerHistory { history_entry_count: usize, /// Messages submitted by the user *during this UI session* (newest at END). - local_history: Vec, + local_history: Vec, /// Cache of persistent history entries fetched on-demand. - fetched_history: HashMap, + fetched_history: HashMap, /// Current cursor within the combined (persistent + local) history. `None` /// indicates the user is *not* currently browsing history. @@ -54,8 +81,8 @@ impl ChatComposerHistory { /// Record a message submitted by the user in the current session so it can /// be recalled later. - pub fn record_local_submission(&mut self, text: &str) { - if text.is_empty() { + pub fn record_local_submission(&mut self, entry: HistoryEntry) { + if entry.text.is_empty() && entry.local_image_paths.is_empty() { return; } @@ -63,11 +90,11 @@ impl ChatComposerHistory { self.last_history_text = None; // Avoid inserting a duplicate if identical to the previous entry. - if self.local_history.last().is_some_and(|prev| prev == text) { + if self.local_history.last().is_some_and(|prev| prev == &entry) { return; } - self.local_history.push(text.to_string()); + self.local_history.push(entry); } /// Reset navigation tracking so the next Up key resumes from the latest entry. @@ -99,7 +126,7 @@ impl ChatComposerHistory { /// Handle . Returns true when the key was consumed and the caller /// should request a redraw. - pub fn navigate_up(&mut self, app_event_tx: &AppEventSender) -> Option { + pub fn navigate_up(&mut self, app_event_tx: &AppEventSender) -> Option { let total_entries = self.history_entry_count + self.local_history.len(); if total_entries == 0 { return None; @@ -116,7 +143,7 @@ impl ChatComposerHistory { } /// Handle . - pub fn navigate_down(&mut self, app_event_tx: &AppEventSender) -> Option { + pub fn navigate_down(&mut self, app_event_tx: &AppEventSender) -> Option { let total_entries = self.history_entry_count + self.local_history.len(); if total_entries == 0 { return None; @@ -137,7 +164,7 @@ impl ChatComposerHistory { // Past newest – clear and exit browsing mode. self.history_cursor = None; self.last_history_text = None; - Some(String::new()) + Some(HistoryEntry::empty()) } } } @@ -148,16 +175,17 @@ impl ChatComposerHistory { log_id: u64, offset: usize, entry: Option, - ) -> Option { + ) -> Option { if self.history_log_id != Some(log_id) { return None; } let text = entry?; - self.fetched_history.insert(offset, text.clone()); + let entry = HistoryEntry::from_text(text); + self.fetched_history.insert(offset, entry.clone()); if self.history_cursor == Some(offset as isize) { - self.last_history_text = Some(text.clone()); - return Some(text); + self.last_history_text = Some(entry.text.clone()); + return Some(entry); } None } @@ -170,19 +198,20 @@ impl ChatComposerHistory { &mut self, global_idx: usize, app_event_tx: &AppEventSender, - ) -> Option { + ) -> Option { if global_idx >= self.history_entry_count { // Local entry. - if let Some(text) = self + if let Some(entry) = self .local_history .get(global_idx - self.history_entry_count) + .cloned() { - self.last_history_text = Some(text.clone()); - return Some(text.clone()); + self.last_history_text = Some(entry.text.clone()); + return Some(entry); } - } else if let Some(text) = self.fetched_history.get(&global_idx) { - self.last_history_text = Some(text.clone()); - return Some(text.clone()); + } else if let Some(entry) = self.fetched_history.get(&global_idx).cloned() { + self.last_history_text = Some(entry.text.clone()); + return Some(entry); } else if let Some(log_id) = self.history_log_id { let op = Op::GetHistoryEntryRequest { offset: global_idx, @@ -206,22 +235,28 @@ mod tests { let mut history = ChatComposerHistory::new(); // Empty submissions are ignored. - history.record_local_submission(""); + history.record_local_submission(HistoryEntry::from_text(String::new())); assert_eq!(history.local_history.len(), 0); // First entry is recorded. - history.record_local_submission("hello"); + history.record_local_submission(HistoryEntry::from_text("hello".to_string())); assert_eq!(history.local_history.len(), 1); - assert_eq!(history.local_history.last().unwrap(), "hello"); + assert_eq!( + history.local_history.last().unwrap(), + &HistoryEntry::from_text("hello".to_string()) + ); // Identical consecutive entry is skipped. - history.record_local_submission("hello"); + history.record_local_submission(HistoryEntry::from_text("hello".to_string())); assert_eq!(history.local_history.len(), 1); // Different entry is recorded. - history.record_local_submission("world"); + history.record_local_submission(HistoryEntry::from_text("world".to_string())); assert_eq!(history.local_history.len(), 2); - assert_eq!(history.local_history.last().unwrap(), "world"); + assert_eq!( + history.local_history.last().unwrap(), + &HistoryEntry::from_text("world".to_string()) + ); } #[test] @@ -252,7 +287,7 @@ mod tests { // Inject the async response. assert_eq!( - Some("latest".into()), + Some(HistoryEntry::from_text("latest".to_string())), history.on_entry_response(1, 2, Some("latest".into())) ); @@ -273,7 +308,7 @@ mod tests { ); assert_eq!( - Some("older".into()), + Some(HistoryEntry::from_text("older".to_string())), history.on_entry_response(1, 1, Some("older".into())) ); } @@ -285,16 +320,29 @@ mod tests { let mut history = ChatComposerHistory::new(); history.set_metadata(1, 3); - history.fetched_history.insert(1, "command2".into()); - history.fetched_history.insert(2, "command3".into()); + history + .fetched_history + .insert(1, HistoryEntry::from_text("command2".to_string())); + history + .fetched_history + .insert(2, HistoryEntry::from_text("command3".to_string())); - assert_eq!(Some("command3".into()), history.navigate_up(&tx)); - assert_eq!(Some("command2".into()), history.navigate_up(&tx)); + assert_eq!( + Some(HistoryEntry::from_text("command3".to_string())), + history.navigate_up(&tx) + ); + assert_eq!( + Some(HistoryEntry::from_text("command2".to_string())), + history.navigate_up(&tx) + ); history.reset_navigation(); assert!(history.history_cursor.is_none()); assert!(history.last_history_text.is_none()); - assert_eq!(Some("command3".into()), history.navigate_up(&tx)); + assert_eq!( + Some(HistoryEntry::from_text("command3".to_string())), + history.navigate_up(&tx) + ); } } diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 3c54dff0fe..e879e880cc 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -33,6 +33,7 @@ pub(crate) struct CommandPopup { pub(crate) struct CommandPopupFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) personality_command_enabled: bool, + pub(crate) windows_degraded_sandbox_active: bool, } impl CommandPopup { @@ -41,6 +42,7 @@ impl CommandPopup { let builtins = slash_commands::builtins_for_input( flags.collaboration_modes_enabled, flags.personality_command_enabled, + flags.windows_degraded_sandbox_active, ); // Exclude prompts that collide with builtin command names and sort by name. let exclude: HashSet = builtins.iter().map(|(n, _)| (*n).to_string()).collect(); @@ -461,6 +463,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: true, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/collab".to_string()); @@ -478,6 +481,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: false, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/pers".to_string()); @@ -503,6 +507,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: true, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/personality".to_string()); diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index e0a0cc2f49..e018c7ff8a 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -43,18 +43,10 @@ impl FileSearchPopup { return; } - // Determine if current matches are still relevant. - let keep_existing = query.starts_with(&self.display_query); - self.pending_query.clear(); self.pending_query.push_str(query); self.waiting = true; // waiting for new results - - if !keep_existing { - self.matches.clear(); - self.state.reset(); - } } /// Put the popup into an "idle" state used for an empty query (just "@"). diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 16f4f22a94..1ded8ce319 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -8,8 +8,30 @@ //! Some footer content is time-based rather than event-based, such as the "press again to quit" //! hint. The owning widgets schedule redraws so time-based hints can expire even if the UI is //! otherwise idle. -#[cfg(target_os = "linux")] -use crate::clipboard_paste::is_probably_wsl; +//! +//! Single-line collapse overview: +//! 1. The composer decides the current `FooterMode` and hint flags, then calls +//! `single_line_footer_layout` for the base single-line modes. +//! 2. `single_line_footer_layout` applies the width-based fallback rules: +//! (If this description is hard to follow, just try it out by resizing +//! your terminal width; these rules were built out of trial and error.) +//! - Start with the fullest left-side hint plus the right-side context. +//! - When the queue hint is active, prefer keeping that queue hint visible, +//! even if it means dropping the right-side context earlier; the queue +//! hint may also be shortened before it is removed. +//! - When the queue hint is not active but the mode cycle hint is applicable, +//! drop "? for shortcuts" before dropping "(shift+tab to cycle)". +//! - If "(shift+tab to cycle)" cannot fit, also hide the right-side +//! context to avoid too many state transitions in quick succession. +//! - Finally, try a mode-only line (with and without context), and fall +//! back to no left-side footer if nothing can fit. +//! 3. When collapse chooses a specific line, callers render it via +//! `render_footer_line`. Otherwise, callers render the straightforward +//! mode-to-text mapping via `render_footer_from_props`. +//! +//! In short: `single_line_footer_layout` chooses *what* best fits, and the two +//! render helpers choose whether to draw the chosen line or the default +//! `FooterProps` mapping. use crate::key_hint; use crate::key_hint::KeyBinding; use crate::render::line_utils::prefix_lines; @@ -27,9 +49,10 @@ use ratatui::widgets::Widget; /// The rendering inputs for the footer area under the composer. /// /// Callers are expected to construct `FooterProps` from higher-level state (`ChatComposer`, -/// `BottomPane`, and `ChatWidget`) and pass it to `render_footer`. The footer treats these values as -/// authoritative and does not attempt to infer missing state (for example, it does not query -/// whether a task is running). +/// `BottomPane`, and `ChatWidget`) and pass it to the footer render helpers +/// (`render_footer_from_props` or the single-line collapse logic). The footer +/// treats these values as authoritative and does not attempt to infer missing +/// state (for example, it does not query whether a task is running). #[derive(Clone, Copy, Debug)] pub(crate) struct FooterProps { pub(crate) mode: FooterMode, @@ -38,6 +61,7 @@ pub(crate) struct FooterProps { 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. /// /// This is rendered when `mode` is `FooterMode::QuitShortcutReminder`. @@ -77,7 +101,7 @@ impl CollaborationModeIndicator { let label = self.label(show_cycle_hint); match self { CollaborationModeIndicator::Plan => Span::from(label).magenta(), - CollaborationModeIndicator::Code => Span::from(label).cyan(), + CollaborationModeIndicator::Code => Span::from(label).dim(), CollaborationModeIndicator::PairProgramming => Span::from(label).cyan(), CollaborationModeIndicator::Execute => Span::from(label).dim(), } @@ -92,21 +116,36 @@ impl CollaborationModeIndicator { pub(crate) enum FooterMode { /// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D). QuitShortcutReminder, - ShortcutSummary, + /// Multi-line shortcut overlay shown after pressing `?`. ShortcutOverlay, + /// Transient "press Esc again" hint shown after the first Esc while idle. EscHint, - ContextOnly, + /// Base single-line footer when the composer is empty. + 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. + ComposerHasDraft, } -pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { +pub(crate) fn toggle_shortcut_mode( + current: FooterMode, + ctrl_c_hint: bool, + is_empty: bool, +) -> FooterMode { if ctrl_c_hint && matches!(current, FooterMode::QuitShortcutReminder) { return current; } + let base_mode = if is_empty { + FooterMode::ComposerEmpty + } else { + FooterMode::ComposerHasDraft + }; + match current { - FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => { - FooterMode::ShortcutSummary - } + FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => base_mode, _ => FooterMode::ShortcutOverlay, } } @@ -124,57 +163,347 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { FooterMode::EscHint | FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder - | FooterMode::ContextOnly => FooterMode::ShortcutSummary, + | FooterMode::ComposerHasDraft => FooterMode::ComposerEmpty, other => other, } } pub(crate) fn footer_height(props: FooterProps) -> u16 { - footer_lines(props).len() as u16 + let show_shortcuts_hint = match props.mode { + FooterMode::ComposerEmpty => true, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match props.mode { + FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::QuitShortcutReminder + | FooterMode::ComposerEmpty + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; + footer_from_props_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16 } -pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) { +/// Render a single precomputed footer line. +pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) { Paragraph::new(prefix_lines( - footer_lines(props), + vec![line], " ".repeat(FOOTER_INDENT_COLS).into(), " ".repeat(FOOTER_INDENT_COLS).into(), )) .render(area, buf); } -pub(crate) fn render_mode_indicator( +/// Render footer content directly from `FooterProps`. +/// +/// This is intentionally not part of the width-based collapse/fallback logic. +/// Transient instructional states (shortcut overlay, Esc hint, quit reminder) +/// prioritize "what to do next" instructions and currently suppress the +/// collaboration mode label entirely. When collapse logic has already chosen a +/// specific single line, prefer `render_footer_line`. +pub(crate) fn render_footer_from_props( area: Rect, buf: &mut Buffer, - indicator: Option, + props: FooterProps, + collaboration_mode_indicator: Option, show_cycle_hint: bool, - left_content_width: Option, + show_shortcuts_hint: bool, + show_queue_hint: bool, ) { - let Some(indicator) = indicator else { - return; + Paragraph::new(prefix_lines( + footer_from_props_lines( + props, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ), + " ".repeat(FOOTER_INDENT_COLS).into(), + " ".repeat(FOOTER_INDENT_COLS).into(), + )) + .render(area, buf); +} + +pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool { + let max_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16); + left_width <= max_width +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SummaryHintKind { + None, + Shortcuts, + QueueMessage, + QueueShort, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct LeftSideState { + hint: SummaryHintKind, + show_cycle_hint: bool, +} + +fn left_side_line( + collaboration_mode_indicator: Option, + state: LeftSideState, +) -> Line<'static> { + let mut line = Line::from(""); + match state.hint { + SummaryHintKind::None => {} + SummaryHintKind::Shortcuts => { + line.push_span(key_hint::plain(KeyCode::Char('?'))); + line.push_span(" for shortcuts".dim()); + } + SummaryHintKind::QueueMessage => { + line.push_span(key_hint::plain(KeyCode::Tab)); + line.push_span(" to queue message".dim()); + } + SummaryHintKind::QueueShort => { + line.push_span(key_hint::plain(KeyCode::Tab)); + line.push_span(" to queue".dim()); + } }; + + if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { + if !matches!(state.hint, SummaryHintKind::None) { + line.push_span(" · ".dim()); + } + line.push_span(collaboration_mode_indicator.styled_span(state.show_cycle_hint)); + } + + line +} + +pub(crate) enum SummaryLeft { + Default, + Custom(Line<'static>), + None, +} + +/// Compute the single-line footer layout and whether the right-side context +/// indicator can be shown alongside it. +pub(crate) fn single_line_footer_layout( + area: Rect, + context_width: u16, + collaboration_mode_indicator: Option, + show_cycle_hint: bool, + show_shortcuts_hint: bool, + show_queue_hint: bool, +) -> (SummaryLeft, bool) { + let hint_kind = if show_queue_hint { + SummaryHintKind::QueueMessage + } else if show_shortcuts_hint { + SummaryHintKind::Shortcuts + } else { + SummaryHintKind::None + }; + let default_state = LeftSideState { + hint: hint_kind, + show_cycle_hint, + }; + let default_line = left_side_line(collaboration_mode_indicator, default_state); + let default_width = default_line.width() as u16; + if default_width > 0 && can_show_left_with_context(area, default_width, context_width) { + return (SummaryLeft::Default, true); + } + + let state_line = |state: LeftSideState| -> Line<'static> { + if state == default_state { + default_line.clone() + } else { + left_side_line(collaboration_mode_indicator, state) + } + }; + let state_width = |state: LeftSideState| -> u16 { state_line(state).width() as u16 }; + // When the mode cycle hint is applicable (idle, non-queue mode), only show + // the right-side context indicator if the "(shift+tab to cycle)" variant + // can also fit. + let context_requires_cycle_hint = show_cycle_hint && !show_queue_hint; + + if show_queue_hint { + // In queue mode, prefer dropping context before dropping the queue hint. + let queue_states = [ + default_state, + LeftSideState { + hint: SummaryHintKind::QueueMessage, + show_cycle_hint: false, + }, + LeftSideState { + hint: SummaryHintKind::QueueShort, + show_cycle_hint: false, + }, + ]; + + // Pass 1: keep the right-side context indicator if any queue variant + // can fit alongside it. We skip adjacent duplicates because + // `default_state` can already be the no-cycle queue variant. + let mut previous_state: Option = None; + for state in queue_states { + if previous_state == Some(state) { + continue; + } + previous_state = Some(state); + let width = state_width(state); + if width > 0 && can_show_left_with_context(area, width, context_width) { + if state == default_state { + return (SummaryLeft::Default, true); + } + return (SummaryLeft::Custom(state_line(state)), true); + } + } + + // Pass 2: if context cannot fit, drop it before dropping the queue + // hint. Reuse the same dedupe so we do not try equivalent states twice. + let mut previous_state: Option = None; + for state in queue_states { + if previous_state == Some(state) { + continue; + } + previous_state = Some(state); + let width = state_width(state); + if width > 0 && left_fits(area, width) { + if state == default_state { + return (SummaryLeft::Default, false); + } + return (SummaryLeft::Custom(state_line(state)), false); + } + } + } else if collaboration_mode_indicator.is_some() { + if show_cycle_hint { + // First fallback: drop shortcut hint but keep the cycle + // hint on the mode label if it can fit. + let cycle_state = LeftSideState { + hint: SummaryHintKind::None, + show_cycle_hint: true, + }; + let cycle_width = state_width(cycle_state); + if cycle_width > 0 && can_show_left_with_context(area, cycle_width, context_width) { + return (SummaryLeft::Custom(state_line(cycle_state)), true); + } + if cycle_width > 0 && left_fits(area, cycle_width) { + return (SummaryLeft::Custom(state_line(cycle_state)), false); + } + } + + // Next fallback: mode label only. If the cycle hint is applicable but + // cannot fit, we also suppress context so the right side does not + // outlive "(shift+tab to cycle)" on the left. + let mode_only_state = LeftSideState { + hint: SummaryHintKind::None, + show_cycle_hint: false, + }; + let mode_only_width = state_width(mode_only_state); + if !context_requires_cycle_hint + && mode_only_width > 0 + && can_show_left_with_context(area, mode_only_width, context_width) + { + return ( + SummaryLeft::Custom(state_line(mode_only_state)), + true, // show_context + ); + } + if mode_only_width > 0 && left_fits(area, mode_only_width) { + return ( + SummaryLeft::Custom(state_line(mode_only_state)), + false, // show_context + ); + } + } + + // Final fallback: if queue variants (or other earlier states) could not fit + // at all, drop every hint and try to show just the mode label. + if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { + let mode_only_state = LeftSideState { + hint: SummaryHintKind::None, + show_cycle_hint: false, + }; + // Compute the width without going through `state_line` so we do not + // depend on `default_state` (which may still be a queue variant). + let mode_only_width = + left_side_line(Some(collaboration_mode_indicator), mode_only_state).width() as u16; + if !context_requires_cycle_hint + && can_show_left_with_context(area, mode_only_width, context_width) + { + return ( + SummaryLeft::Custom(left_side_line( + Some(collaboration_mode_indicator), + mode_only_state, + )), + true, // show_context + ); + } + if left_fits(area, mode_only_width) { + return ( + SummaryLeft::Custom(left_side_line( + Some(collaboration_mode_indicator), + mode_only_state, + )), + false, // show_context + ); + } + } + + (SummaryLeft::None, true) +} + +fn right_aligned_x(area: Rect, content_width: u16) -> Option { + if area.is_empty() { + return None; + } + + let right_padding = FOOTER_INDENT_COLS as u16; + let max_width = area.width.saturating_sub(right_padding); + if content_width == 0 || max_width == 0 { + return None; + } + + if content_width >= max_width { + return Some(area.x.saturating_add(right_padding)); + } + + Some( + area.x + .saturating_add(area.width) + .saturating_sub(content_width) + .saturating_sub(right_padding), + ) +} + +pub(crate) fn can_show_left_with_context(area: Rect, left_width: u16, context_width: u16) -> bool { + let Some(context_x) = right_aligned_x(area, context_width) else { + return true; + }; + let left_extent = FOOTER_INDENT_COLS as u16 + left_width; + left_extent <= context_x.saturating_sub(area.x) +} + +pub(crate) fn render_context_right(area: Rect, buf: &mut Buffer, line: &Line<'static>) { if area.is_empty() { return; } - let span = indicator.styled_span(show_cycle_hint); - let label_width = span.width() as u16; - if label_width == 0 || label_width > area.width { + let context_width = line.width() as u16; + let Some(mut x) = right_aligned_x(area, context_width) else { return; - } - - let x = area - .x - .saturating_add(area.width) - .saturating_sub(label_width) - .saturating_sub(FOOTER_INDENT_COLS as u16); + }; let y = area.y + area.height.saturating_sub(1); - if let Some(left_content_width) = left_content_width { - let left_extent = FOOTER_INDENT_COLS as u16 + left_content_width; - if left_extent >= x.saturating_sub(area.x) { - return; + let max_x = area.x.saturating_add(area.width); + + for span in &line.spans { + if x >= max_x { + break; } + let span_width = span.width() as u16; + if span_width == 0 { + continue; + } + let remaining = max_x.saturating_sub(x); + let draw_width = span_width.min(remaining); + buf.set_span(x, y, span, draw_width); + x = x.saturating_add(span_width); } - buf.set_span(x, y, &span, label_width); } pub(crate) fn inset_footer_hint_area(mut area: Rect) -> Rect { @@ -193,62 +522,77 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S footer_hint_items_line(items).render(inset_footer_hint_area(area), buf); } -fn footer_lines(props: FooterProps) -> Vec> { - // Show the context indicator on the left, appended after the primary hint - // (e.g., "? for shortcuts"). Keep it visible even when typing (i.e., when - // the shortcut hint is hidden). Hide it only for the multi-line - // ShortcutOverlay. +/// Map `FooterProps` to footer lines without width-based collapse. +/// +/// This is the canonical FooterMode-to-text mapping. It powers transient, +/// instructional states (shortcut overlay, Esc hint, quit reminder) and also +/// the default rendering for base states when collapse is not applied (or when +/// `single_line_footer_layout` returns `SummaryLeft::Default`). Collapse and +/// fallback decisions live in `single_line_footer_layout`; this function only +/// formats the chosen/default content. +fn footer_from_props_lines( + props: FooterProps, + collaboration_mode_indicator: Option, + show_cycle_hint: bool, + show_shortcuts_hint: bool, + show_queue_hint: bool, +) -> Vec> { match props.mode { FooterMode::QuitShortcutReminder => { vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } - FooterMode::ShortcutSummary => { - let mut line = context_window_line( - props.context_window_percent, - props.context_window_used_tokens, - ); - line.push_span(" · ".dim()); - line.extend(vec![ - key_hint::plain(KeyCode::Char('?')).into(), - " for shortcuts".dim(), - ]); - vec![line] + FooterMode::ComposerEmpty => { + let state = LeftSideState { + hint: if show_shortcuts_hint { + SummaryHintKind::Shortcuts + } else { + SummaryHintKind::None + }, + show_cycle_hint, + }; + vec![left_side_line(collaboration_mode_indicator, state)] } FooterMode::ShortcutOverlay => { - #[cfg(target_os = "linux")] - let is_wsl = is_probably_wsl(); - #[cfg(not(target_os = "linux"))] - let is_wsl = false; - let state = ShortcutsState { use_shift_enter_hint: props.use_shift_enter_hint, esc_backtrack_hint: props.esc_backtrack_hint, - is_wsl, + is_wsl: props.is_wsl, collaboration_modes_enabled: props.collaboration_modes_enabled, }; shortcut_overlay_lines(state) } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], - FooterMode::ContextOnly => { - let mut line = context_window_line( - props.context_window_percent, - props.context_window_used_tokens, - ); - if props.is_task_running && props.steer_enabled { - line.push_span(" · ".dim()); - line.push_span(key_hint::plain(KeyCode::Tab)); - line.push_span(" to queue message".dim()); - } - vec![line] + FooterMode::ComposerHasDraft => { + let state = LeftSideState { + hint: if show_queue_hint { + SummaryHintKind::QueueMessage + } else { + SummaryHintKind::None + }, + show_cycle_hint, + }; + vec![left_side_line(collaboration_mode_indicator, state)] } } } -pub(crate) fn footer_line_width(props: FooterProps) -> u16 { - footer_lines(props) - .last() - .map(|line| line.width() as u16) - .unwrap_or(0) +pub(crate) fn footer_line_width( + props: FooterProps, + collaboration_mode_indicator: Option, + show_cycle_hint: bool, + show_shortcuts_hint: bool, + show_queue_hint: bool, +) -> u16 { + footer_from_props_lines( + props, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ) + .last() + .map(|line| line.width() as u16) + .unwrap_or(0) } pub(crate) fn footer_hint_items_width(items: &[(String, String)]) -> u16 { @@ -396,7 +740,7 @@ fn build_columns(entries: Vec>) -> Vec> { .collect() } -fn context_window_line(percent: Option, used_tokens: Option) -> Line<'static> { +pub(crate) fn context_window_line(percent: Option, used_tokens: Option) -> Line<'static> { if let Some(percent) = percent { let percent = percent.clamp(0, 100); return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]); @@ -611,40 +955,107 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[ mod tests { use super::*; use insta::assert_snapshot; + use pretty_assertions::assert_eq; use ratatui::Terminal; use ratatui::backend::TestBackend; fn snapshot_footer(name: &str, props: FooterProps) { - let height = footer_height(props).max(1); - let mut terminal = Terminal::new(TestBackend::new(80, height)).unwrap(); - terminal - .draw(|f| { - let area = Rect::new(0, 0, f.area().width, height); - render_footer(area, f.buffer_mut(), props); - }) - .unwrap(); - assert_snapshot!(name, terminal.backend()); + snapshot_footer_with_mode_indicator(name, 80, props, None); } - fn snapshot_footer_with_indicator( + fn snapshot_footer_with_mode_indicator( name: &str, width: u16, props: FooterProps, - indicator: Option, + collaboration_mode_indicator: Option, ) { let height = footer_height(props).max(1); let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); terminal .draw(|f| { let area = Rect::new(0, 0, f.area().width, height); - render_footer(area, f.buffer_mut(), props); - render_mode_indicator( - area, - f.buffer_mut(), - indicator, - !props.is_task_running, - Some(footer_line_width(props)), + let context_line = context_window_line( + props.context_window_percent, + props.context_window_used_tokens, ); + let context_width = context_line.width() as u16; + let show_cycle_hint = !props.is_task_running; + let show_shortcuts_hint = match props.mode { + FooterMode::ComposerEmpty => true, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match props.mode { + FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::QuitShortcutReminder + | FooterMode::ComposerEmpty + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; + let left_width = footer_line_width( + props, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + let can_show_left_and_context = + can_show_left_with_context(area, left_width, context_width); + if matches!( + props.mode, + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft + ) { + let (summary_left, show_context) = single_line_footer_layout( + area, + context_width, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + match summary_left { + SummaryLeft::Default => { + render_footer_from_props( + area, + f.buffer_mut(), + props, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + } + SummaryLeft::Custom(line) => { + render_footer_line(area, f.buffer_mut(), line); + } + SummaryLeft::None => {} + } + if show_context { + render_context_right(area, f.buffer_mut(), &context_line); + } + } else { + render_footer_from_props( + area, + f.buffer_mut(), + props, + collaboration_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + let show_context = can_show_left_and_context + && !matches!( + props.mode, + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + ); + if show_context { + render_context_right(area, f.buffer_mut(), &context_line); + } + } }) .unwrap(); assert_snapshot!(name, terminal.backend()); @@ -655,12 +1066,13 @@ mod tests { snapshot_footer( "footer_shortcuts_default", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, 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')), context_window_percent: None, context_window_used_tokens: None, @@ -676,6 +1088,7 @@ mod tests { is_task_running: false, 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, @@ -691,6 +1104,7 @@ mod tests { is_task_running: false, steer_enabled: false, collaboration_modes_enabled: true, + is_wsl: false, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), context_window_percent: None, context_window_used_tokens: None, @@ -706,6 +1120,7 @@ mod tests { is_task_running: false, 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, @@ -721,6 +1136,7 @@ mod tests { 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, @@ -736,6 +1152,7 @@ mod tests { is_task_running: false, 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, @@ -751,6 +1168,7 @@ mod tests { is_task_running: false, 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, @@ -760,12 +1178,13 @@ mod tests { snapshot_footer( "footer_shortcuts_context_running", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, 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: Some(72), context_window_used_tokens: None, @@ -775,12 +1194,13 @@ mod tests { snapshot_footer( "footer_context_tokens_used", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, 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')), context_window_percent: None, context_window_used_tokens: Some(123_456), @@ -788,14 +1208,15 @@ mod tests { ); snapshot_footer( - "footer_context_only_queue_hint_disabled", + "footer_composer_has_draft_queue_hint_disabled", FooterProps { - mode: FooterMode::ContextOnly, + 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, @@ -803,14 +1224,15 @@ mod tests { ); snapshot_footer( - "footer_context_only_queue_hint_enabled", + "footer_composer_has_draft_queue_hint_enabled", FooterProps { - mode: FooterMode::ContextOnly, + mode: FooterMode::ComposerHasDraft, 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')), context_window_percent: None, context_window_used_tokens: None, @@ -818,25 +1240,26 @@ mod tests { ); let props = FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, 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')), context_window_percent: None, context_window_used_tokens: None, }; - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_wide", 120, props, Some(CollaborationModeIndicator::Plan), ); - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_narrow_overlap_hides", 50, props, @@ -844,22 +1267,60 @@ mod tests { ); let props = FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, 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')), context_window_percent: None, context_window_used_tokens: None, }; - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_running_hides_hint", 120, props, Some(CollaborationModeIndicator::Plan), ); } + + #[test] + fn paste_image_shortcut_prefers_ctrl_alt_v_under_wsl() { + let descriptor = SHORTCUTS + .iter() + .find(|descriptor| descriptor.id == ShortcutId::PasteImage) + .expect("paste image shortcut"); + + let is_wsl = { + #[cfg(target_os = "linux")] + { + crate::clipboard_paste::is_probably_wsl() + } + #[cfg(not(target_os = "linux"))] + { + false + } + }; + + let expected_key = if is_wsl { + key_hint::ctrl_alt(KeyCode::Char('v')) + } else { + key_hint::ctrl(KeyCode::Char('v')) + }; + + let actual_key = descriptor + .binding_for(ShortcutsState { + use_shift_enter_hint: false, + esc_backtrack_hint: false, + is_wsl, + collaboration_modes_enabled: false, + }) + .expect("shortcut binding") + .key; + + assert_eq!(actual_key, expected_key); + } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index d0236062f6..701e0762cb 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -213,6 +213,12 @@ impl BottomPane { self.request_redraw(); } + #[cfg(target_os = "windows")] + pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) { + self.composer.set_windows_degraded_sandbox_active(enabled); + self.request_redraw(); + } + pub fn set_collaboration_mode_indicator( &mut self, indicator: Option, @@ -263,7 +269,10 @@ impl BottomPane { let (ctrl_c_completed, view_complete, view_in_paste_burst) = { let last_index = self.view_stack.len() - 1; let view = &mut self.view_stack[last_index]; + let prefer_esc = + key_event.code == KeyCode::Esc && view.prefer_esc_to_handle_key_event(); let ctrl_c_completed = key_event.code == KeyCode::Esc + && !prefer_esc && matches!(view.on_ctrl_c(), CancellationEvent::Handled) && view.is_complete(); if ctrl_c_completed { @@ -332,6 +341,7 @@ impl BottomPane { self.on_active_view_complete(); } self.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c'))); + self.request_redraw(); } event } else if self.composer_is_empty() { @@ -340,6 +350,7 @@ impl BottomPane { self.view_stack.pop(); self.clear_composer_for_ctrl_c(); self.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c'))); + self.request_redraw(); CancellationEvent::Handled } } @@ -809,7 +820,9 @@ mod tests { use insta::assert_snapshot; use ratatui::buffer::Buffer; use ratatui::layout::Rect; + use std::cell::Cell; use std::path::PathBuf; + use std::rc::Rc; use tokio::sync::mpsc::unbounded_channel; fn snapshot_buffer(buf: &Buffer) -> String { @@ -1148,6 +1161,7 @@ mod tests { description: "test skill".to_string(), short_description: None, interface: None, + dependencies: None, path: PathBuf::from("test-skill"), scope: SkillScope::User, }]), @@ -1235,4 +1249,63 @@ mod tests { "expected Esc to send Op::Interrupt while a task is running" ); } + + #[test] + fn esc_routes_to_handle_key_event_when_requested() { + #[derive(Default)] + struct EscRoutingView { + on_ctrl_c_calls: Rc>, + handle_calls: Rc>, + } + + impl Renderable for EscRoutingView { + fn render(&self, _area: Rect, _buf: &mut Buffer) {} + + fn desired_height(&self, _width: u16) -> u16 { + 0 + } + } + + impl BottomPaneView for EscRoutingView { + fn handle_key_event(&mut self, _key_event: KeyEvent) { + self.handle_calls + .set(self.handle_calls.get().saturating_add(1)); + } + + fn on_ctrl_c(&mut self) -> CancellationEvent { + self.on_ctrl_c_calls + .set(self.on_ctrl_c_calls.get().saturating_add(1)); + CancellationEvent::Handled + } + + fn prefer_esc_to_handle_key_event(&self) -> bool { + true + } + } + + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + frame_requester: FrameRequester::test_dummy(), + has_input_focus: true, + enhanced_keys_supported: false, + placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, + animations_enabled: true, + skills: Some(Vec::new()), + }); + + let on_ctrl_c_calls = Rc::new(Cell::new(0)); + let handle_calls = Rc::new(Cell::new(0)); + pane.push_view(Box::new(EscRoutingView { + on_ctrl_c_calls: Rc::clone(&on_ctrl_c_calls), + handle_calls: Rc::clone(&handle_calls), + })); + + pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert_eq!(on_ctrl_c_calls.get(), 0); + assert_eq!(handle_calls.get(), 1); + } } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/layout.rs b/codex-rs/tui/src/bottom_pane/request_user_input/layout.rs index 4a1817da89..27d53229b6 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/layout.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/layout.rs @@ -1,16 +1,14 @@ use ratatui::layout::Rect; +use super::DESIRED_SPACERS_BETWEEN_SECTIONS; use super::RequestUserInputOverlay; pub(super) struct LayoutSections { pub(super) progress_area: Rect, - pub(super) header_area: Rect, pub(super) question_area: Rect, - pub(super) answer_title_area: Rect, // Wrapped question text lines to render in the question area. pub(super) question_lines: Vec, pub(super) options_area: Rect, - pub(super) notes_title_area: Rect, pub(super) notes_area: Rect, // Number of footer rows (status + hints). pub(super) footer_lines: u16, @@ -20,26 +18,22 @@ impl RequestUserInputOverlay { /// Compute layout sections, collapsing notes and hints as space shrinks. pub(super) fn layout_sections(&self, area: Rect) -> LayoutSections { let has_options = self.has_options(); - let footer_pref = if self.unanswered_count() > 0 { 2 } else { 1 }; + let notes_visible = !has_options || self.notes_ui_visible(); + let footer_pref = self.footer_required_height(area.width); let notes_pref_height = self.notes_input_height(area.width); let mut question_lines = self.wrapped_question_lines(area.width); let question_height = question_lines.len() as u16; - let ( - question_height, - progress_height, - answer_title_height, - notes_title_height, - notes_height, - options_height, - footer_lines, - ) = if has_options { + let layout = if has_options { self.layout_with_options( - area.height, - area.width, - question_height, - notes_pref_height, - footer_pref, + OptionsLayoutArgs { + available_height: area.height, + width: area.width, + question_height, + notes_pref_height, + footer_pref, + notes_visible, + }, &mut question_lines, ) } else { @@ -52,130 +46,95 @@ impl RequestUserInputOverlay { ) }; - let ( - progress_area, - header_area, - question_area, - answer_title_area, - options_area, - notes_title_area, - notes_area, - ) = self.build_layout_areas( - area, - LayoutHeights { - progress_height, - question_height, - answer_title_height, - options_height, - notes_title_height, - notes_height, - }, - ); + let (progress_area, question_area, options_area, notes_area) = + self.build_layout_areas(area, layout); LayoutSections { progress_area, - header_area, question_area, - answer_title_area, question_lines, options_area, - notes_title_area, notes_area, - footer_lines, + footer_lines: layout.footer_lines, } } /// Layout calculation when options are present. - /// - /// Handles both tight layout (when space is constrained) and normal layout - /// (when there's sufficient space for all elements). - /// - /// Returns: (question_height, progress_height, answer_title_height, notes_title_height, notes_height, options_height, footer_lines) fn layout_with_options( &self, - available_height: u16, - width: u16, - question_height: u16, - notes_pref_height: u16, - footer_pref: u16, + args: OptionsLayoutArgs, question_lines: &mut Vec, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let options_required_height = self.options_required_height(width); - let min_options_height = 1u16; - let required = 1u16 - .saturating_add(question_height) - .saturating_add(options_required_height); - - if required > available_height { - self.layout_with_options_tight( + ) -> LayoutPlan { + let OptionsLayoutArgs { + available_height, + width, + mut question_height, + notes_pref_height, + footer_pref, + notes_visible, + } = args; + let min_options_height = available_height.min(1); + let max_question_height = available_height.saturating_sub(min_options_height); + if question_height > max_question_height { + question_height = max_question_height; + question_lines.truncate(question_height as usize); + } + self.layout_with_options_normal( + OptionsNormalArgs { available_height, question_height, - min_options_height, - question_lines, - ) - } else { - self.layout_with_options_normal( - available_height, - question_height, - options_required_height, notes_pref_height, footer_pref, - ) - } + notes_visible, + }, + OptionsHeights { + preferred: self.options_preferred_height(width), + full: self.options_required_height(width), + }, + ) } - /// Tight layout for options case: allocate header + question + options first - /// and drop everything else when space is constrained. - fn layout_with_options_tight( - &self, - available_height: u16, - question_height: u16, - min_options_height: u16, - question_lines: &mut Vec, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let max_question_height = - available_height.saturating_sub(1u16.saturating_add(min_options_height)); - let adjusted_question_height = question_height.min(max_question_height); - question_lines.truncate(adjusted_question_height as usize); - let options_height = - available_height.saturating_sub(1u16.saturating_add(adjusted_question_height)); - - (adjusted_question_height, 0, 0, 0, 0, options_height, 0) - } - - /// Normal layout for options case: allocate space for all elements with - /// preference order: notes, footer, labels, then progress. + /// Normal layout for options case: allocate footer + progress first, and + /// only allocate notes (and its label) when explicitly visible. fn layout_with_options_normal( &self, - available_height: u16, - question_height: u16, - options_required_height: u16, - notes_pref_height: u16, - footer_pref: u16, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let options_height = options_required_height; - let used = 1u16 - .saturating_add(question_height) - .saturating_add(options_height); + args: OptionsNormalArgs, + options: OptionsHeights, + ) -> LayoutPlan { + let OptionsNormalArgs { + available_height, + question_height, + notes_pref_height, + footer_pref, + notes_visible, + } = args; + let max_options_height = available_height.saturating_sub(question_height); + let min_options_height = max_options_height.min(1); + let mut options_height = options + .preferred + .min(max_options_height) + .max(min_options_height); + let used = question_height.saturating_add(options_height); let mut remaining = available_height.saturating_sub(used); - // Prefer notes next, then footer, then labels, with progress last. - let mut notes_height = notes_pref_height.min(remaining); - remaining = remaining.saturating_sub(notes_height); - - let footer_lines = footer_pref.min(remaining); - remaining = remaining.saturating_sub(footer_lines); - - let mut answer_title_height = 0; - if remaining > 0 { - answer_title_height = 1; - remaining = remaining.saturating_sub(1); - } - - let mut notes_title_height = 0; - if remaining > 0 { - notes_title_height = 1; - remaining = remaining.saturating_sub(1); + // When notes are hidden, prefer to reserve room for progress, footer, + // and spacers by shrinking the options window if needed. + let desired_spacers = if notes_visible { + // Notes already separate options from the footer, so only keep a + // single spacer between the question and options. + 1 + } else { + DESIRED_SPACERS_BETWEEN_SECTIONS + }; + let required_extra = footer_pref + .saturating_add(1) // progress line + .saturating_add(desired_spacers); + if remaining < required_extra { + let deficit = required_extra.saturating_sub(remaining); + let reducible = options_height.saturating_sub(min_options_height); + let reduce_by = deficit.min(reducible); + options_height = options_height.saturating_sub(reduce_by); + remaining = remaining.saturating_add(reduce_by); } let mut progress_height = 0; @@ -184,18 +143,56 @@ impl RequestUserInputOverlay { remaining = remaining.saturating_sub(1); } - // Expand the notes composer with any leftover rows. + if !notes_visible { + let mut spacer_after_options = 0; + if remaining > footer_pref { + spacer_after_options = 1; + remaining = remaining.saturating_sub(1); + } + let footer_lines = footer_pref.min(remaining); + remaining = remaining.saturating_sub(footer_lines); + let mut spacer_after_question = 0; + if remaining > 0 { + spacer_after_question = 1; + remaining = remaining.saturating_sub(1); + } + let grow_by = remaining.min(options.full.saturating_sub(options_height)); + options_height = options_height.saturating_add(grow_by); + return LayoutPlan { + question_height, + progress_height, + spacer_after_question, + options_height, + spacer_after_options, + notes_height: 0, + footer_lines, + }; + } + + let footer_lines = footer_pref.min(remaining); + remaining = remaining.saturating_sub(footer_lines); + + // Prefer spacers before notes, then notes. + let mut spacer_after_question = 0; + if remaining > 0 { + spacer_after_question = 1; + remaining = remaining.saturating_sub(1); + } + let spacer_after_options = 0; + let mut notes_height = notes_pref_height.min(remaining); + remaining = remaining.saturating_sub(notes_height); + notes_height = notes_height.saturating_add(remaining); - ( + LayoutPlan { question_height, progress_height, - answer_title_height, - notes_title_height, - notes_height, + spacer_after_question, options_height, + spacer_after_options, + notes_height, footer_lines, - ) + } } /// Layout calculation when no options are present. @@ -203,7 +200,6 @@ impl RequestUserInputOverlay { /// Handles both tight layout (when space is constrained) and normal layout /// (when there's sufficient space for all elements). /// - /// Returns: (question_height, progress_height, answer_title_height, notes_title_height, notes_height, options_height, footer_lines) fn layout_without_options( &self, available_height: u16, @@ -211,8 +207,8 @@ impl RequestUserInputOverlay { notes_pref_height: u16, footer_pref: u16, question_lines: &mut Vec, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let required = 1u16.saturating_add(question_height); + ) -> LayoutPlan { + let required = question_height; if required > available_height { self.layout_without_options_tight(available_height, question_height, question_lines) } else { @@ -231,12 +227,20 @@ impl RequestUserInputOverlay { available_height: u16, question_height: u16, question_lines: &mut Vec, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let max_question_height = available_height.saturating_sub(1); + ) -> LayoutPlan { + let max_question_height = available_height; let adjusted_question_height = question_height.min(max_question_height); question_lines.truncate(adjusted_question_height as usize); - (adjusted_question_height, 0, 0, 0, 0, 0, 0) + LayoutPlan { + question_height: adjusted_question_height, + progress_height: 0, + spacer_after_question: 0, + options_height: 0, + spacer_after_options: 0, + notes_height: 0, + footer_lines: 0, + } } /// Normal layout for no-options case: allocate space for notes, footer, and progress. @@ -246,8 +250,8 @@ impl RequestUserInputOverlay { question_height: u16, notes_pref_height: u16, footer_pref: u16, - ) -> (u16, u16, u16, u16, u16, u16, u16) { - let required = 1u16.saturating_add(question_height); + ) -> LayoutPlan { + let required = question_height; let mut remaining = available_height.saturating_sub(required); let mut notes_height = notes_pref_height.min(remaining); remaining = remaining.saturating_sub(notes_height); @@ -263,29 +267,26 @@ impl RequestUserInputOverlay { notes_height = notes_height.saturating_add(remaining); - ( + LayoutPlan { question_height, progress_height, - 0, - 0, + spacer_after_question: 0, + options_height: 0, + spacer_after_options: 0, notes_height, - 0, footer_lines, - ) + } } /// Build the final layout areas from computed heights. fn build_layout_areas( &self, area: Rect, - heights: LayoutHeights, + heights: LayoutPlan, ) -> ( Rect, // progress_area - Rect, // header_area Rect, // question_area - Rect, // answer_title_area Rect, // options_area - Rect, // notes_title_area Rect, // notes_area ) { let mut cursor_y = area.y; @@ -296,14 +297,6 @@ impl RequestUserInputOverlay { height: heights.progress_height, }; cursor_y = cursor_y.saturating_add(heights.progress_height); - let header_height = area.height.saturating_sub(heights.progress_height).min(1); - let header_area = Rect { - x: area.x, - y: cursor_y, - width: area.width, - height: header_height, - }; - cursor_y = cursor_y.saturating_add(header_height); let question_area = Rect { x: area.x, y: cursor_y, @@ -311,14 +304,8 @@ impl RequestUserInputOverlay { height: heights.question_height, }; cursor_y = cursor_y.saturating_add(heights.question_height); + cursor_y = cursor_y.saturating_add(heights.spacer_after_question); - let answer_title_area = Rect { - x: area.x, - y: cursor_y, - width: area.width, - height: heights.answer_title_height, - }; - cursor_y = cursor_y.saturating_add(heights.answer_title_height); let options_area = Rect { x: area.x, y: cursor_y, @@ -326,14 +313,8 @@ impl RequestUserInputOverlay { height: heights.options_height, }; cursor_y = cursor_y.saturating_add(heights.options_height); + cursor_y = cursor_y.saturating_add(heights.spacer_after_options); - let notes_title_area = Rect { - x: area.x, - y: cursor_y, - width: area.width, - height: heights.notes_title_height, - }; - cursor_y = cursor_y.saturating_add(heights.notes_title_height); let notes_area = Rect { x: area.x, y: cursor_y, @@ -341,24 +322,42 @@ impl RequestUserInputOverlay { height: heights.notes_height, }; - ( - progress_area, - header_area, - question_area, - answer_title_area, - options_area, - notes_title_area, - notes_area, - ) + (progress_area, question_area, options_area, notes_area) } } #[derive(Clone, Copy, Debug)] -struct LayoutHeights { +struct LayoutPlan { progress_height: u16, question_height: u16, - answer_title_height: u16, + spacer_after_question: u16, options_height: u16, - notes_title_height: u16, + spacer_after_options: u16, notes_height: u16, + footer_lines: u16, +} + +#[derive(Clone, Copy, Debug)] +struct OptionsLayoutArgs { + available_height: u16, + width: u16, + question_height: u16, + notes_pref_height: u16, + footer_pref: u16, + notes_visible: bool, +} + +#[derive(Clone, Copy, Debug)] +struct OptionsNormalArgs { + available_height: u16, + question_height: u16, + notes_pref_height: u16, + footer_pref: u16, + notes_visible: bool, +} + +#[derive(Clone, Copy, Debug)] +struct OptionsHeights { + preferred: u16, + full: u16, } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 1da955ec3b..979499835d 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -13,6 +13,7 @@ use std::path::PathBuf; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; mod layout; mod render; @@ -33,12 +34,23 @@ use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputEvent; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::TextElement; +use unicode_width::UnicodeWidthStr; -const NOTES_PLACEHOLDER: &str = "Add notes (optional)"; +const NOTES_PLACEHOLDER: &str = "Add notes"; const ANSWER_PLACEHOLDER: &str = "Type your answer (optional)"; // Keep in sync with ChatComposer's minimum composer height. const MIN_COMPOSER_HEIGHT: u16 = 3; -const SELECT_OPTION_PLACEHOLDER: &str = "Select an option to add notes (optional)"; +const SELECT_OPTION_PLACEHOLDER: &str = "Select an option to add notes"; +pub(super) const TIP_SEPARATOR: &str = " | "; +pub(super) const DESIRED_SPACERS_BETWEEN_SECTIONS: u16 = 2; +const OTHER_OPTION_LABEL: &str = "None of the above"; +const OTHER_OPTION_DESCRIPTION: &str = "Optionally, add details in notes (tab)."; +const UNANSWERED_CONFIRM_TITLE: &str = "Submit with unanswered questions?"; +const UNANSWERED_CONFIRM_GO_BACK: &str = "Go back"; +const UNANSWERED_CONFIRM_GO_BACK_DESC: &str = "Return to the first unanswered question."; +const UNANSWERED_CONFIRM_SUBMIT: &str = "Proceed"; +const UNANSWERED_CONFIRM_SUBMIT_DESC_SINGULAR: &str = "question"; +const UNANSWERED_CONFIRM_SUBMIT_DESC_PLURAL: &str = "questions"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Focus { @@ -46,20 +58,63 @@ enum Focus { Notes, } -#[derive(Default, Clone)] +#[derive(Default, Clone, PartialEq)] struct ComposerDraft { text: String, text_elements: Vec, local_image_paths: Vec, + pending_pastes: Vec<(String, String)>, +} + +impl ComposerDraft { + fn text_with_pending(&self) -> String { + if self.pending_pastes.is_empty() { + return self.text.clone(); + } + debug_assert!( + !self.text_elements.is_empty(), + "pending pastes should always have matching text elements" + ); + let (expanded, _) = ChatComposer::expand_pending_pastes( + &self.text, + self.text_elements.clone(), + &self.pending_pastes, + ); + expanded + } } struct AnswerState { - // Final selection for the question (always set for option questions). - selected: Option, // Scrollable cursor state for option navigation/highlight. - option_state: ScrollState, + options_state: ScrollState, // Per-question notes draft. draft: ComposerDraft, + // Whether the answer for this question has been explicitly submitted. + answer_committed: bool, + // Whether the notes UI has been explicitly opened for this question. + notes_visible: bool, +} + +#[derive(Clone, Debug)] +pub(super) struct FooterTip { + pub(super) text: String, + pub(super) highlight: bool, +} + +impl FooterTip { + fn new(text: impl Into) -> Self { + Self { + text: text.into(), + highlight: false, + } + } + + fn highlighted(text: impl Into) -> Self { + Self { + text: text.into(), + highlight: true, + } + } } pub(crate) struct RequestUserInputOverlay { @@ -75,6 +130,8 @@ pub(crate) struct RequestUserInputOverlay { current_idx: usize, focus: Focus, done: bool, + pending_submission_draft: Option, + confirm_unanswered: Option, } impl RequestUserInputOverlay { @@ -106,6 +163,8 @@ impl RequestUserInputOverlay { current_idx: 0, focus: Focus::Options, done: false, + pending_submission_draft: None, + confirm_unanswered: None, }; overlay.reset_for_request(); overlay.ensure_focus_available(); @@ -145,25 +204,45 @@ impl RequestUserInputOverlay { fn options_len(&self) -> usize { self.current_question() - .and_then(|question| question.options.as_ref()) - .map(std::vec::Vec::len) + .map(Self::options_len_for_question) .unwrap_or(0) } + fn option_index_for_digit(&self, ch: char) -> Option { + if !self.has_options() { + return None; + } + let digit = ch.to_digit(10)?; + if digit == 0 { + return None; + } + let idx = (digit - 1) as usize; + (idx < self.options_len()).then_some(idx) + } + fn selected_option_index(&self) -> Option { if !self.has_options() { return None; } self.current_answer() - .and_then(|answer| answer.selected.or(answer.option_state.selected_idx)) + .and_then(|answer| answer.options_state.selected_idx) } - fn current_option_label(&self) -> Option<&str> { - let idx = self.selected_option_index()?; - self.current_question() - .and_then(|question| question.options.as_ref()) - .and_then(|options| options.get(idx)) - .map(|option| option.label.as_str()) + fn notes_has_content(&self, idx: usize) -> bool { + if idx == self.current_index() { + !self.composer.current_text_with_pending().trim().is_empty() + } else { + !self.answers[idx].draft.text.trim().is_empty() + } + } + + pub(super) fn notes_ui_visible(&self) -> bool { + if !self.has_options() { + return true; + } + let idx = self.current_index(); + self.current_answer() + .is_some_and(|answer| answer.notes_visible || self.notes_has_content(idx)) } pub(super) fn wrapped_question_lines(&self, width: u16) -> Vec { @@ -177,26 +256,50 @@ impl RequestUserInputOverlay { .unwrap_or_default() } + fn focus_is_notes(&self) -> bool { + matches!(self.focus, Focus::Notes) + } + + fn confirm_unanswered_active(&self) -> bool { + self.confirm_unanswered.is_some() + } + pub(super) fn option_rows(&self) -> Vec { self.current_question() - .and_then(|question| question.options.as_ref()) - .map(|options| { - options + .and_then(|question| question.options.as_ref().map(|options| (question, options))) + .map(|(question, options)| { + let selected_idx = self + .current_answer() + .and_then(|answer| answer.options_state.selected_idx); + let mut rows = options .iter() .enumerate() .map(|(idx, opt)| { - let selected = self - .current_answer() - .and_then(|answer| answer.selected) - .is_some_and(|sel| sel == idx); - let prefix = if selected { "(x)" } else { "( )" }; + let selected = selected_idx.is_some_and(|sel| sel == idx); + let prefix = if selected { '›' } else { ' ' }; + let label = opt.label.as_str(); + let number = idx + 1; GenericDisplayRow { - name: format!("{prefix} {}", opt.label), + name: format!("{prefix} {number}. {label}"), description: Some(opt.description.clone()), ..Default::default() } }) - .collect::>() + .collect::>(); + + if Self::other_option_enabled_for_question(question) { + let idx = options.len(); + let selected = selected_idx.is_some_and(|sel| sel == idx); + let prefix = if selected { '›' } else { ' ' }; + let number = idx + 1; + rows.push(GenericDisplayRow { + name: format!("{prefix} {number}. {OTHER_OPTION_LABEL}"), + description: Some(OTHER_OPTION_DESCRIPTION.to_string()), + ..Default::default() + }); + } + + rows }) .unwrap_or_default() } @@ -213,7 +316,28 @@ impl RequestUserInputOverlay { let mut state = self .current_answer() - .map(|answer| answer.option_state) + .map(|answer| answer.options_state) + .unwrap_or_default(); + if state.selected_idx.is_none() { + state.selected_idx = Some(0); + } + + measure_rows_height(&rows, &state, rows.len(), width.max(1)) + } + + pub(super) fn options_preferred_height(&self, width: u16) -> u16 { + if !self.has_options() { + return 0; + } + + let rows = self.option_rows(); + if rows.is_empty() { + return 1; + } + + let mut state = self + .current_answer() + .map(|answer| answer.options_state) .unwrap_or_default(); if state.selected_idx.is_none() { state.selected_idx = Some(0); @@ -224,7 +348,7 @@ impl RequestUserInputOverlay { fn capture_composer_draft(&self) -> ComposerDraft { ComposerDraft { - text: self.composer.current_text_with_pending(), + text: self.composer.current_text(), text_elements: self.composer.text_elements(), local_image_paths: self .composer @@ -232,13 +356,21 @@ impl RequestUserInputOverlay { .into_iter() .map(|img| img.path) .collect(), + pending_pastes: self.composer.pending_pastes(), } } fn save_current_draft(&mut self) { let draft = self.capture_composer_draft(); + let notes_empty = draft.text.trim().is_empty(); if let Some(answer) = self.current_answer_mut() { + if answer.answer_committed && answer.draft != draft { + answer.answer_committed = false; + } answer.draft = draft; + if !notes_empty { + answer.notes_visible = true; + } } } @@ -255,15 +387,12 @@ impl RequestUserInputOverlay { let draft = answer.draft.clone(); self.composer .set_text_content(draft.text, draft.text_elements, draft.local_image_paths); + self.composer.set_pending_pastes(draft.pending_pastes); self.composer.move_cursor_to_end(); } fn notes_placeholder(&self) -> &'static str { - if self.has_options() - && self - .current_answer() - .is_some_and(|answer| answer.selected.is_none()) - { + if self.has_options() && self.selected_option_index().is_none() { SELECT_OPTION_PLACEHOLDER } else if self.has_options() { NOTES_PLACEHOLDER @@ -272,6 +401,119 @@ impl RequestUserInputOverlay { } } + fn sync_composer_placeholder(&mut self) { + self.composer + .set_placeholder_text(self.notes_placeholder().to_string()); + } + + fn clear_notes_draft(&mut self) { + if let Some(answer) = self.current_answer_mut() { + answer.draft = ComposerDraft::default(); + answer.answer_committed = false; + answer.notes_visible = true; + } + self.pending_submission_draft = None; + self.composer + .set_text_content(String::new(), Vec::new(), Vec::new()); + self.composer.move_cursor_to_end(); + self.sync_composer_placeholder(); + } + + fn footer_tips(&self) -> Vec { + let mut tips = Vec::new(); + let notes_visible = self.notes_ui_visible(); + if self.has_options() { + if self.selected_option_index().is_some() && !notes_visible { + tips.push(FooterTip::highlighted("tab to add notes")); + } + if self.selected_option_index().is_some() && notes_visible && self.focus_is_notes() { + tips.push(FooterTip::new("tab to clear notes")); + } + } + + let question_count = self.question_count(); + let is_last_question = self.current_index().saturating_add(1) >= question_count; + let enter_tip = if question_count == 1 { + FooterTip::highlighted("enter to submit answer") + } else if is_last_question { + FooterTip::highlighted("enter to submit all") + } else { + FooterTip::new("enter to submit answer") + }; + tips.push(enter_tip); + if question_count > 1 { + if is_last_question { + tips.push(FooterTip::new("ctrl + n first question")); + } else { + tips.push(FooterTip::new("ctrl + n next question")); + } + } + tips.push(FooterTip::new("esc to interrupt")); + tips + } + + pub(super) fn footer_tip_lines(&self, width: u16) -> Vec> { + self.wrap_footer_tips(width, self.footer_tips()) + } + + pub(super) fn footer_tip_lines_with_prefix( + &self, + width: u16, + prefix: Option, + ) -> Vec> { + let mut tips = Vec::new(); + if let Some(prefix) = prefix { + tips.push(prefix); + } + tips.extend(self.footer_tips()); + self.wrap_footer_tips(width, tips) + } + + fn wrap_footer_tips(&self, width: u16, tips: Vec) -> Vec> { + let max_width = width.max(1) as usize; + let separator_width = UnicodeWidthStr::width(TIP_SEPARATOR); + if tips.is_empty() { + return vec![Vec::new()]; + } + + let mut lines: Vec> = Vec::new(); + let mut current: Vec = Vec::new(); + let mut used = 0usize; + + for tip in tips { + let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(max_width); + let extra = if current.is_empty() { + tip_width + } else { + separator_width.saturating_add(tip_width) + }; + if !current.is_empty() && used.saturating_add(extra) > max_width { + lines.push(current); + current = Vec::new(); + used = 0; + } + if current.is_empty() { + used = tip_width; + } else { + used = used + .saturating_add(separator_width) + .saturating_add(tip_width); + } + current.push(tip); + } + + if current.is_empty() { + lines.push(Vec::new()); + } else { + lines.push(current); + } + lines + } + + pub(super) fn footer_required_height(&self, width: u16) -> u16 { + self.footer_tip_lines(width).len() as u16 + } + /// Ensure the focus mode is valid for the current question. fn ensure_focus_available(&mut self) { if self.question_count() == 0 { @@ -279,6 +521,14 @@ impl RequestUserInputOverlay { } if !self.has_options() { self.focus = Focus::Notes; + if let Some(answer) = self.current_answer_mut() { + answer.notes_visible = true; + } + return; + } + if matches!(self.focus, Focus::Notes) && !self.notes_ui_visible() { + self.focus = Focus::Options; + self.sync_composer_placeholder(); } } @@ -289,16 +539,19 @@ impl RequestUserInputOverlay { .questions .iter() .map(|question| { - let mut option_state = ScrollState::new(); - if let Some(options) = question.options.as_ref() - && !options.is_empty() - { - option_state.selected_idx = Some(0); + let has_options = question + .options + .as_ref() + .is_some_and(|options| !options.is_empty()); + let mut options_state = ScrollState::new(); + if has_options { + options_state.selected_idx = Some(0); } AnswerState { - selected: option_state.selected_idx, - option_state, + options_state, draft: ComposerDraft::default(), + answer_committed: false, + notes_visible: !has_options, } }) .collect(); @@ -307,6 +560,47 @@ impl RequestUserInputOverlay { self.focus = Focus::Options; self.composer .set_text_content(String::new(), Vec::new(), Vec::new()); + self.confirm_unanswered = None; + self.pending_submission_draft = None; + } + + fn options_len_for_question( + question: &codex_protocol::request_user_input::RequestUserInputQuestion, + ) -> usize { + let options_len = question + .options + .as_ref() + .map(std::vec::Vec::len) + .unwrap_or(0); + if Self::other_option_enabled_for_question(question) { + options_len + 1 + } else { + options_len + } + } + + fn other_option_enabled_for_question( + question: &codex_protocol::request_user_input::RequestUserInputQuestion, + ) -> bool { + question.is_other + && question + .options + .as_ref() + .is_some_and(|options| !options.is_empty()) + } + + fn option_label_for_index( + question: &codex_protocol::request_user_input::RequestUserInputQuestion, + idx: usize, + ) -> Option { + let options = question.options.as_ref()?; + if idx < options.len() { + return options.get(idx).map(|opt| opt.label.clone()); + } + if idx == options.len() && Self::other_option_enabled_for_question(question) { + return Some(OTHER_OPTION_LABEL.to_string()); + } + None } /// Move to the next/previous question, wrapping in either direction. @@ -318,38 +612,73 @@ impl RequestUserInputOverlay { self.save_current_draft(); let offset = if next { 1 } else { len.saturating_sub(1) }; self.current_idx = (self.current_idx + offset) % len; - self.ensure_focus_available(); self.restore_current_draft(); + self.ensure_focus_available(); + } + + fn jump_to_question(&mut self, idx: usize) { + if idx >= self.question_count() { + return; + } + self.save_current_draft(); + self.current_idx = idx; + self.restore_current_draft(); + self.ensure_focus_available(); } /// Synchronize selection state to the currently focused option. - fn select_current_option(&mut self) { + fn select_current_option(&mut self, committed: bool) { if !self.has_options() { return; } let options_len = self.options_len(); - let Some(answer) = self.current_answer_mut() else { - return; + let updated = if let Some(answer) = self.current_answer_mut() { + answer.options_state.clamp_selection(options_len); + answer.answer_committed = committed; + true + } else { + false }; - answer.option_state.clamp_selection(options_len); - answer.selected = answer.option_state.selected_idx; + if updated { + self.sync_composer_placeholder(); + } + } + + /// Clear the current option selection and hide notes when empty. + fn clear_selection(&mut self) { + if !self.has_options() { + return; + } + if let Some(answer) = self.current_answer_mut() { + answer.options_state.reset(); + answer.draft = ComposerDraft::default(); + answer.answer_committed = false; + answer.notes_visible = false; + } + self.pending_submission_draft = None; + self.composer + .set_text_content(String::new(), Vec::new(), Vec::new()); + self.composer.move_cursor_to_end(); + self.sync_composer_placeholder(); } /// Ensure there is a selection before allowing notes entry. fn ensure_selected_for_notes(&mut self) { - if self.has_options() - && self - .current_answer() - .is_some_and(|answer| answer.selected.is_none()) - { - self.select_current_option(); + if let Some(answer) = self.current_answer_mut() { + answer.notes_visible = true; } + self.sync_composer_placeholder(); } /// Advance to next question, or submit when on the last one. fn go_next_or_submit(&mut self) { if self.current_index() + 1 >= self.question_count() { - self.submit_answers(); + self.save_current_draft(); + if self.unanswered_count() > 0 { + self.open_unanswered_confirmation(); + } else { + self.submit_answers(); + } } else { self.move_question(true); } @@ -357,28 +686,28 @@ impl RequestUserInputOverlay { /// Build the response payload and dispatch it to the app. fn submit_answers(&mut self) { + self.confirm_unanswered = None; self.save_current_draft(); let mut answers = HashMap::new(); for (idx, question) in self.request.questions.iter().enumerate() { let answer_state = &self.answers[idx]; let options = question.options.as_ref(); - // For option questions we always produce a selection. - let selected_idx = if options.is_some_and(|opts| !opts.is_empty()) { - answer_state - .selected - .or(answer_state.option_state.selected_idx) + // For option questions we may still produce no selection. + let selected_idx = + if options.is_some_and(|opts| !opts.is_empty()) && answer_state.answer_committed { + answer_state.options_state.selected_idx + } else { + None + }; + // Notes are appended as extra answers. For freeform questions, only submit when + // the user explicitly committed the draft. + let notes = if answer_state.answer_committed { + answer_state.draft.text_with_pending().trim().to_string() } else { - answer_state.selected + String::new() }; - // Notes are appended as extra answers. - let notes = answer_state.draft.text.trim().to_string(); - let selected_label = selected_idx.and_then(|selected_idx| { - question - .options - .as_ref() - .and_then(|opts| opts.get(selected_idx)) - .map(|opt| opt.label.clone()) - }); + let selected_label = selected_idx + .and_then(|selected_idx| Self::option_label_for_index(question, selected_idx)); let mut answer_list = selected_label.into_iter().collect::>(); if !notes.is_empty() { answer_list.push(format!("user_note: {notes}")); @@ -405,27 +734,97 @@ impl RequestUserInputOverlay { } } - /// Count freeform-only questions that have no notes. + fn open_unanswered_confirmation(&mut self) { + let mut state = ScrollState::new(); + state.selected_idx = Some(0); + self.confirm_unanswered = Some(state); + } + + fn close_unanswered_confirmation(&mut self) { + self.confirm_unanswered = None; + } + + fn unanswered_question_count(&self) -> usize { + self.unanswered_count() + } + + fn unanswered_submit_description(&self) -> String { + let count = self.unanswered_question_count(); + let suffix = if count == 1 { + UNANSWERED_CONFIRM_SUBMIT_DESC_SINGULAR + } else { + UNANSWERED_CONFIRM_SUBMIT_DESC_PLURAL + }; + format!("Submit with {count} unanswered {suffix}.") + } + + fn first_unanswered_index(&self) -> Option { + let current_text = self.composer.current_text(); + self.request + .questions + .iter() + .enumerate() + .find(|(idx, _)| !self.is_question_answered(*idx, ¤t_text)) + .map(|(idx, _)| idx) + } + + fn unanswered_confirmation_rows(&self) -> Vec { + let selected = self + .confirm_unanswered + .as_ref() + .and_then(|state| state.selected_idx) + .unwrap_or(0); + let entries = [ + ( + UNANSWERED_CONFIRM_SUBMIT, + self.unanswered_submit_description(), + ), + ( + UNANSWERED_CONFIRM_GO_BACK, + UNANSWERED_CONFIRM_GO_BACK_DESC.to_string(), + ), + ]; + entries + .iter() + .enumerate() + .map(|(idx, (label, description))| { + let prefix = if idx == selected { '›' } else { ' ' }; + let number = idx + 1; + GenericDisplayRow { + name: format!("{prefix} {number}. {label}"), + description: Some(description.clone()), + ..Default::default() + } + }) + .collect() + } + + fn is_question_answered(&self, idx: usize, _current_text: &str) -> bool { + let Some(question) = self.request.questions.get(idx) else { + return false; + }; + let Some(answer) = self.answers.get(idx) else { + return false; + }; + let has_options = question + .options + .as_ref() + .is_some_and(|options| !options.is_empty()); + if has_options { + answer.options_state.selected_idx.is_some() && answer.answer_committed + } else { + answer.answer_committed + } + } + + /// Count questions that would submit an empty answer list. fn unanswered_count(&self) -> usize { let current_text = self.composer.current_text(); self.request .questions .iter() .enumerate() - .filter(|(idx, question)| { - let answer = &self.answers[*idx]; - let options = question.options.as_ref(); - if options.is_some_and(|opts| !opts.is_empty()) { - false - } else { - let notes = if *idx == self.current_index() { - current_text.as_str() - } else { - answer.draft.text.as_str() - }; - notes.trim().is_empty() - } - }) + .filter(|(idx, _question)| !self.is_question_answered(*idx, ¤t_text)) .count() } @@ -449,6 +848,7 @@ impl RequestUserInputOverlay { text: text.clone(), text_elements: text_elements.clone(), local_image_paths: local_image_paths.clone(), + pending_pastes: Vec::new(), }; } self.composer @@ -457,6 +857,17 @@ impl RequestUserInputOverlay { self.composer.set_footer_hint_override(Some(Vec::new())); } + fn apply_submission_draft(&mut self, draft: ComposerDraft) { + if let Some(answer) = self.current_answer_mut() { + answer.draft = draft.clone(); + } + self.composer + .set_text_content(draft.text, draft.text_elements, draft.local_image_paths); + self.composer.set_pending_pastes(draft.pending_pastes); + self.composer.move_cursor_to_end(); + self.composer.set_footer_hint_override(Some(Vec::new())); + } + fn handle_composer_input_result(&mut self, result: InputResult) -> bool { match result { InputResult::Submitted { @@ -467,21 +878,93 @@ impl RequestUserInputOverlay { text, text_elements, } => { - self.apply_submission_to_draft(text, text_elements); + if self.has_options() + && matches!(self.focus, Focus::Notes) + && !text.trim().is_empty() + { + let options_len = self.options_len(); + if let Some(answer) = self.current_answer_mut() { + answer.options_state.clamp_selection(options_len); + } + } + if self.has_options() { + if let Some(answer) = self.current_answer_mut() { + answer.answer_committed = true; + } + } else if let Some(answer) = self.current_answer_mut() { + answer.answer_committed = !text.trim().is_empty(); + } + let draft_override = self.pending_submission_draft.take(); + if let Some(draft) = draft_override { + self.apply_submission_draft(draft); + } else { + self.apply_submission_to_draft(text, text_elements); + } self.go_next_or_submit(); true } _ => false, } } + + fn handle_confirm_unanswered_key_event(&mut self, key_event: KeyEvent) { + if key_event.kind == KeyEventKind::Release { + return; + } + let Some(state) = self.confirm_unanswered.as_mut() else { + return; + }; + + match key_event.code { + KeyCode::Esc | KeyCode::Backspace => { + self.close_unanswered_confirmation(); + if let Some(idx) = self.first_unanswered_index() { + self.jump_to_question(idx); + } + } + KeyCode::Up | KeyCode::Char('k') => { + state.move_up_wrap(2); + } + KeyCode::Down | KeyCode::Char('j') => { + state.move_down_wrap(2); + } + KeyCode::Enter => { + let selected = state.selected_idx.unwrap_or(0); + self.close_unanswered_confirmation(); + if selected == 0 { + self.submit_answers(); + } else if let Some(idx) = self.first_unanswered_index() { + self.jump_to_question(idx); + } + } + KeyCode::Char('1') | KeyCode::Char('2') => { + let idx = if matches!(key_event.code, KeyCode::Char('1')) { + 0 + } else { + 1 + }; + state.selected_idx = Some(idx); + } + _ => {} + } + } } impl BottomPaneView for RequestUserInputOverlay { + fn prefer_esc_to_handle_key_event(&self) -> bool { + true + } + fn handle_key_event(&mut self, key_event: KeyEvent) { if key_event.kind == KeyEventKind::Release { return; } + if self.confirm_unanswered_active() { + self.handle_confirm_unanswered_key_event(key_event); + return; + } + if matches!(key_event.code, KeyCode::Esc) { self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); self.done = true; @@ -489,12 +972,46 @@ impl BottomPaneView for RequestUserInputOverlay { } // Question navigation is always available. - match key_event.code { - KeyCode::PageUp => { + match key_event { + KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::CONTROL, + .. + } + | KeyEvent { + code: KeyCode::PageUp, + modifiers: KeyModifiers::NONE, + .. + } => { self.move_question(false); return; } - KeyCode::PageDown => { + KeyEvent { + code: KeyCode::PageDown, + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { + code: KeyCode::Char('n'), + modifiers: KeyModifiers::CONTROL, + .. + } => { + self.move_question(true); + return; + } + KeyEvent { + code: KeyCode::Char('h'), + modifiers: KeyModifiers::NONE, + .. + } if self.has_options() && matches!(self.focus, Focus::Options) => { + self.move_question(false); + return; + } + KeyEvent { + code: KeyCode::Char('l'), + modifiers: KeyModifiers::NONE, + .. + } if self.has_options() && matches!(self.focus, Focus::Options) => { self.move_question(true); return; } @@ -506,40 +1023,95 @@ impl BottomPaneView for RequestUserInputOverlay { let options_len = self.options_len(); // Keep selection synchronized as the user moves. match key_event.code { - KeyCode::Up => { - if let Some(answer) = self.current_answer_mut() { - answer.option_state.move_up_wrap(options_len); - answer.selected = answer.option_state.selected_idx; + KeyCode::Up | KeyCode::Char('k') => { + let moved = if let Some(answer) = self.current_answer_mut() { + answer.options_state.move_up_wrap(options_len); + answer.answer_committed = false; + true + } else { + false + }; + if moved { + self.sync_composer_placeholder(); } } - KeyCode::Down => { - if let Some(answer) = self.current_answer_mut() { - answer.option_state.move_down_wrap(options_len); - answer.selected = answer.option_state.selected_idx; + KeyCode::Down | KeyCode::Char('j') => { + let moved = if let Some(answer) = self.current_answer_mut() { + answer.options_state.move_down_wrap(options_len); + answer.answer_committed = false; + true + } else { + false + }; + if moved { + self.sync_composer_placeholder(); } } KeyCode::Char(' ') => { - self.select_current_option(); + self.select_current_option(true); + } + KeyCode::Backspace | KeyCode::Delete => { + self.clear_selection(); + } + KeyCode::Tab => { + if self.selected_option_index().is_some() { + self.focus = Focus::Notes; + self.ensure_selected_for_notes(); + } } KeyCode::Enter => { - self.select_current_option(); + let has_selection = self.selected_option_index().is_some(); + if has_selection { + self.select_current_option(true); + } self.go_next_or_submit(); } - KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete => { - // Any typing while in options switches to notes for fast freeform input. - self.focus = Focus::Notes; - self.ensure_selected_for_notes(); - let (result, _) = self.composer.handle_key_event(key_event); - self.handle_composer_input_result(result); + KeyCode::Char(ch) => { + if let Some(option_idx) = self.option_index_for_digit(ch) { + if let Some(answer) = self.current_answer_mut() { + answer.options_state.selected_idx = Some(option_idx); + } + self.select_current_option(true); + self.go_next_or_submit(); + } } _ => {} } } Focus::Notes => { + let notes_empty = self.composer.current_text_with_pending().trim().is_empty(); + if self.has_options() && matches!(key_event.code, KeyCode::Tab) { + if let Some(answer) = self.current_answer_mut() { + answer.draft = ComposerDraft::default(); + answer.answer_committed = false; + answer.notes_visible = false; + } + self.composer + .set_text_content(String::new(), Vec::new(), Vec::new()); + self.composer.move_cursor_to_end(); + self.focus = Focus::Options; + self.sync_composer_placeholder(); + return; + } + if self.has_options() && matches!(key_event.code, KeyCode::Backspace) && notes_empty + { + self.save_current_draft(); + if let Some(answer) = self.current_answer_mut() { + answer.notes_visible = false; + } + self.focus = Focus::Options; + self.sync_composer_placeholder(); + return; + } if matches!(key_event.code, KeyCode::Enter) { self.ensure_selected_for_notes(); + self.pending_submission_draft = Some(self.capture_composer_draft()); let (result, _) = self.composer.handle_key_event(key_event); if !self.handle_composer_input_result(result) { + self.pending_submission_draft = None; + if self.has_options() { + self.select_current_option(true); + } self.go_next_or_submit(); } return; @@ -548,15 +1120,27 @@ impl BottomPaneView for RequestUserInputOverlay { let options_len = self.options_len(); match key_event.code { KeyCode::Up => { - if let Some(answer) = self.current_answer_mut() { - answer.option_state.move_up_wrap(options_len); - answer.selected = answer.option_state.selected_idx; + let moved = if let Some(answer) = self.current_answer_mut() { + answer.options_state.move_up_wrap(options_len); + answer.answer_committed = false; + true + } else { + false + }; + if moved { + self.sync_composer_placeholder(); } } KeyCode::Down => { - if let Some(answer) = self.current_answer_mut() { - answer.option_state.move_down_wrap(options_len); - answer.selected = answer.option_state.selected_idx; + let moved = if let Some(answer) = self.current_answer_mut() { + answer.options_state.move_down_wrap(options_len); + answer.answer_committed = false; + true + } else { + false + }; + if moved { + self.sync_composer_placeholder(); } } _ => {} @@ -564,13 +1148,40 @@ impl BottomPaneView for RequestUserInputOverlay { return; } self.ensure_selected_for_notes(); + if matches!( + key_event.code, + KeyCode::Char(_) | KeyCode::Backspace | KeyCode::Delete + ) && let Some(answer) = self.current_answer_mut() + { + answer.answer_committed = false; + } + let before = self.capture_composer_draft(); let (result, _) = self.composer.handle_key_event(key_event); - self.handle_composer_input_result(result); + let submitted = self.handle_composer_input_result(result); + if !submitted { + let after = self.capture_composer_draft(); + if before != after + && let Some(answer) = self.current_answer_mut() + { + answer.answer_committed = false; + } + } } } } fn on_ctrl_c(&mut self) -> CancellationEvent { + if self.confirm_unanswered_active() { + self.close_unanswered_confirmation(); + self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.done = true; + return CancellationEvent::Handled; + } + if self.focus_is_notes() && !self.composer.current_text_with_pending().is_empty() { + self.clear_notes_draft(); + return CancellationEvent::Handled; + } + self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); self.done = true; CancellationEvent::Handled @@ -589,6 +1200,9 @@ impl BottomPaneView for RequestUserInputOverlay { self.focus = Focus::Notes; } self.ensure_selected_for_notes(); + if let Some(answer) = self.current_answer_mut() { + answer.answer_committed = false; + } self.composer.handle_paste(pasted) } @@ -613,6 +1227,7 @@ impl BottomPaneView for RequestUserInputOverlay { mod tests { use super::*; use crate::app_event::AppEvent; + use crate::bottom_pane::selection_popup_common::menu_surface_inset; use crate::render::renderable::Renderable; use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; @@ -620,6 +1235,7 @@ mod tests { use ratatui::buffer::Buffer; use ratatui::layout::Rect; use tokio::sync::mpsc::unbounded_channel; + use unicode_width::UnicodeWidthStr; fn test_sender() -> ( AppEventSender, @@ -652,6 +1268,29 @@ mod tests { } } + fn question_with_options_and_other(id: &str, header: &str) -> RequestUserInputQuestion { + RequestUserInputQuestion { + id: id.to_string(), + header: header.to_string(), + question: "Choose an option.".to_string(), + is_other: true, + options: Some(vec![ + RequestUserInputQuestionOption { + label: "Option 1".to_string(), + description: "First choice.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Option 2".to_string(), + description: "Second choice.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Option 3".to_string(), + description: "Third choice.".to_string(), + }, + ]), + } + } + fn question_with_wrapped_options(id: &str, header: &str) -> RequestUserInputQuestion { RequestUserInputQuestion { id: id.to_string(), @@ -747,7 +1386,7 @@ mod tests { } #[test] - fn options_always_return_a_selection() { + fn options_can_submit_empty_when_unanswered() { let (tx, mut rx) = test_sender(); let mut overlay = RequestUserInputOverlay::new( request_event("turn-1", vec![question_with_options("q1", "Pick one")]), @@ -765,9 +1404,490 @@ mod tests { }; assert_eq!(id, "turn-1"); let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, Vec::::new()); + } + + #[test] + fn enter_commits_default_selection_on_last_option_question() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); assert_eq!(answer.answers, vec!["Option 1".to_string()]); } + #[test] + fn enter_commits_default_selection_on_non_last_option_question() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(overlay.current_index(), 1); + let first_answer = &overlay.answers[0]; + assert!(first_answer.answer_committed); + assert_eq!(first_answer.options_state.selected_idx, Some(0)); + assert!(rx.try_recv().is_err()); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, vec!["Option 1".to_string()]); + } + + #[test] + fn number_keys_select_and_submit_options() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('2'))); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, vec!["Option 2".to_string()]); + } + + #[test] + fn vim_keys_move_option_selection() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer().expect("answer missing"); + assert_eq!(answer.options_state.selected_idx, Some(0)); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('j'))); + let answer = overlay.current_answer().expect("answer missing"); + assert_eq!(answer.options_state.selected_idx, Some(1)); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('k'))); + let answer = overlay.current_answer().expect("answer missing"); + assert_eq!(answer.options_state.selected_idx, Some(0)); + } + + #[test] + fn typing_in_options_does_not_open_notes() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + + assert_eq!(overlay.current_index(), 0); + assert_eq!(overlay.notes_ui_visible(), false); + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('x'))); + assert_eq!(overlay.current_index(), 0); + assert_eq!(overlay.notes_ui_visible(), false); + assert!(matches!(overlay.focus, Focus::Options)); + assert_eq!(overlay.composer.current_text_with_pending(), ""); + } + + #[test] + fn h_l_move_between_questions_in_options() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + + assert_eq!(overlay.current_index(), 0); + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('l'))); + assert_eq!(overlay.current_index(), 1); + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('h'))); + assert_eq!(overlay.current_index(), 0); + } + + #[test] + fn tab_opens_notes_when_option_selected() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(1); + + assert_eq!(overlay.notes_ui_visible(), false); + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + assert_eq!(overlay.notes_ui_visible(), true); + assert!(matches!(overlay.focus, Focus::Notes)); + } + + #[test] + fn switching_to_options_resets_notes_focus_when_notes_hidden() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_without_options("q1", "Notes"), + question_with_options("q2", "Pick one"), + ], + ), + tx, + true, + false, + false, + ); + + assert!(matches!(overlay.focus, Focus::Notes)); + overlay.move_question(true); + + assert!(matches!(overlay.focus, Focus::Options)); + assert_eq!(overlay.notes_ui_visible(), false); + } + + #[test] + fn switching_from_freeform_with_text_resets_focus_and_keeps_last_option_empty() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_without_options("q1", "Notes"), + question_with_options("q2", "Pick one"), + ], + ), + tx, + true, + false, + false, + ); + + overlay + .composer + .set_text_content("freeform notes".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + + overlay.move_question(true); + + assert!(matches!(overlay.focus, Focus::Options)); + assert_eq!(overlay.notes_ui_visible(), false); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert!(overlay.confirm_unanswered_active()); + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('1'))); + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, Vec::::new()); + let answer = response.answers.get("q2").expect("answer missing"); + assert_eq!(answer.answers, vec!["Option 1".to_string()]); + } + + #[test] + fn esc_in_notes_mode_without_options_interrupts() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_without_options("q1", "Notes")]), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert_eq!(overlay.done, true); + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(op) = event else { + panic!("expected CodexOp"); + }; + assert_eq!(op, Op::Interrupt); + } + + #[test] + fn esc_in_options_mode_interrupts() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert_eq!(overlay.done, true); + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(op) = event else { + panic!("expected CodexOp"); + }; + assert_eq!(op, Op::Interrupt); + } + + #[test] + fn esc_in_notes_mode_interrupts() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert_eq!(overlay.done, true); + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(op) = event else { + panic!("expected CodexOp"); + }; + assert_eq!(op, Op::Interrupt); + } + + #[test] + fn esc_in_notes_mode_interrupts_with_notes_visible() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + overlay.handle_key_event(KeyEvent::from(KeyCode::Char('a'))); + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert_eq!(overlay.done, true); + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(op) = event else { + panic!("expected CodexOp"); + }; + assert_eq!(op, Op::Interrupt); + } + + #[test] + fn backspace_in_options_clears_selection() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(1); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Backspace)); + + let answer = overlay.current_answer().expect("answer missing"); + assert_eq!(answer.options_state.selected_idx, None); + assert_eq!(overlay.notes_ui_visible(), false); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn backspace_on_empty_notes_closes_notes_ui() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + assert!(matches!(overlay.focus, Focus::Notes)); + assert_eq!(overlay.notes_ui_visible(), true); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Backspace)); + + let answer = overlay.current_answer().expect("answer missing"); + assert!(matches!(overlay.focus, Focus::Options)); + assert_eq!(overlay.notes_ui_visible(), false); + assert_eq!(answer.options_state.selected_idx, Some(0)); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn tab_in_notes_clears_notes_and_hides_ui() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + overlay + .composer + .set_text_content("Some notes".to_string(), Vec::new(), Vec::new()); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + + let answer = overlay.current_answer().expect("answer missing"); + assert!(matches!(overlay.focus, Focus::Options)); + assert_eq!(overlay.notes_ui_visible(), false); + assert_eq!(overlay.composer.current_text_with_pending(), ""); + assert_eq!(answer.draft.text, ""); + assert_eq!(answer.options_state.selected_idx, Some(0)); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn skipped_option_questions_count_as_unanswered() { + let (tx, _rx) = test_sender(); + let overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + + assert_eq!(overlay.unanswered_count(), 1); + } + + #[test] + fn highlighted_option_questions_are_unanswered() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + assert_eq!(overlay.unanswered_count(), 1); + } + + #[test] + fn freeform_requires_enter_with_text_to_mark_answered() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_without_options("q1", "Notes"), + question_without_options("q2", "More"), + ], + ), + tx, + true, + false, + false, + ); + + overlay + .composer + .set_text_content("Draft".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + assert_eq!(overlay.unanswered_count(), 2); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_eq!(overlay.answers[0].answer_committed, true); + assert_eq!(overlay.unanswered_count(), 1); + } + + #[test] + fn freeform_enter_with_empty_text_is_unanswered() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_without_options("q1", "Notes"), + question_without_options("q2", "More"), + ], + ), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_eq!(overlay.answers[0].answer_committed, false); + assert_eq!(overlay.unanswered_count(), 2); + } + #[test] fn freeform_questions_submit_empty_when_empty() { let (tx, mut rx) = test_sender(); @@ -789,6 +1909,73 @@ mod tests { assert_eq!(answer.answers, Vec::::new()); } + #[test] + fn freeform_draft_is_not_submitted_without_enter() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_without_options("q1", "Notes")]), + tx, + true, + false, + false, + ); + overlay + .composer + .set_text_content("Draft text".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + + overlay.submit_answers(); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, Vec::::new()); + } + + #[test] + fn freeform_commit_resets_when_draft_changes() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_without_options("q1", "Notes"), + question_without_options("q2", "More"), + ], + ), + tx, + true, + false, + false, + ); + + overlay + .composer + .set_text_content("Committed".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(overlay.answers[0].answer_committed, true); + + overlay.move_question(false); + overlay + .composer + .set_text_content("Edited".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + overlay.move_question(true); + assert_eq!(overlay.answers[0].answer_committed, false); + + overlay.submit_answers(); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!(answer.answers, Vec::::new()); + } + #[test] fn notes_are_captured_for_selected_option() { let (tx, mut rx) = test_sender(); @@ -802,13 +1989,18 @@ mod tests { { let answer = overlay.current_answer_mut().expect("answer missing"); - answer.option_state.selected_idx = Some(1); + answer.options_state.selected_idx = Some(1); } - overlay.select_current_option(); + overlay.select_current_option(false); overlay .composer .set_text_content("Notes for option 2".to_string(), Vec::new(), Vec::new()); overlay.composer.move_cursor_to_end(); + let draft = overlay.capture_composer_draft(); + if let Some(answer) = overlay.current_answer_mut() { + answer.draft = draft; + answer.answer_committed = true; + } overlay.submit_answers(); @@ -826,6 +2018,91 @@ mod tests { ); } + #[test] + fn notes_submission_commits_selected_option() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Down)); + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + overlay + .composer + .set_text_content("Notes".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_eq!(overlay.current_index(), 1); + let answer = overlay.answers.first().expect("answer missing"); + assert_eq!(answer.options_state.selected_idx, Some(1)); + assert!(answer.answer_committed); + } + + #[test] + fn is_other_adds_none_of_the_above_and_submits_it() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![question_with_options_and_other("q1", "Pick one")], + ), + tx, + true, + false, + false, + ); + + let rows = overlay.option_rows(); + let other_row = rows.last().expect("expected none-of-the-above row"); + assert_eq!(other_row.name, " 4. None of the above"); + assert_eq!( + other_row.description.as_deref(), + Some(OTHER_OPTION_DESCRIPTION) + ); + + let other_idx = overlay.options_len().saturating_sub(1); + { + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(other_idx); + } + overlay + .composer + .set_text_content("Custom answer".to_string(), Vec::new(), Vec::new()); + overlay.composer.move_cursor_to_end(); + let draft = overlay.capture_composer_draft(); + if let Some(answer) = overlay.current_answer_mut() { + answer.draft = draft; + answer.answer_committed = true; + } + + overlay.submit_answers(); + + let event = rx.try_recv().expect("expected AppEvent"); + let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + panic!("expected UserInputAnswer"); + }; + let answer = response.answers.get("q1").expect("answer missing"); + assert_eq!( + answer.answers, + vec![ + OTHER_OPTION_LABEL.to_string(), + "user_note: Custom answer".to_string(), + ] + ); + } + #[test] fn large_paste_is_preserved_when_switching_questions() { let (tx, _rx) = test_sender(); @@ -847,7 +2124,42 @@ mod tests { overlay.composer.handle_paste(large.clone()); overlay.move_question(true); - assert_eq!(overlay.answers[0].draft.text, large); + let draft = &overlay.answers[0].draft; + assert_eq!(draft.pending_pastes.len(), 1); + assert_eq!(draft.pending_pastes[0].1, large); + assert!(draft.text.contains(&draft.pending_pastes[0].0)); + assert_eq!(draft.text_with_pending(), large); + } + + #[test] + fn pending_paste_placeholder_survives_submission_and_back_navigation() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "First"), + question_with_options("q2", "Second"), + ], + ), + tx, + true, + false, + false, + ); + + let large = "x".repeat(1_200); + overlay.focus = Focus::Notes; + overlay.ensure_selected_for_notes(); + overlay.composer.handle_paste(large.clone()); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + overlay.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)); + + let draft = &overlay.answers[0].draft; + assert_eq!(draft.pending_pastes.len(), 1); + assert!(draft.text.contains(&draft.pending_pastes[0].0)); + assert_eq!(draft.text_with_pending(), large); } #[test] @@ -860,13 +2172,36 @@ mod tests { false, false, ); - let area = Rect::new(0, 0, 64, 16); + let area = Rect::new(0, 0, 120, 16); insta::assert_snapshot!( "request_user_input_options", render_snapshot(&overlay, area) ); } + #[test] + fn request_user_input_options_notes_visible_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "Area")]), + tx, + true, + false, + false, + ); + { + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + } + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + + let area = Rect::new(0, 0, 120, 16); + insta::assert_snapshot!( + "request_user_input_options_notes_visible", + render_snapshot(&overlay, area) + ); + } + #[test] fn request_user_input_tight_height_snapshot() { let (tx, _rx) = test_sender(); @@ -877,7 +2212,7 @@ mod tests { false, false, ); - let area = Rect::new(0, 0, 60, 8); + let area = Rect::new(0, 0, 120, 10); insta::assert_snapshot!( "request_user_input_tight_height", render_snapshot(&overlay, area) @@ -901,17 +2236,19 @@ mod tests { let width = 48u16; let question_height = overlay.wrapped_question_lines(width).len() as u16; let options_height = overlay.options_required_height(width); - let height = 1u16 - .saturating_add(question_height) + let extras = 1u16 // progress + .saturating_add(DESIRED_SPACERS_BETWEEN_SECTIONS) + .saturating_add(overlay.footer_required_height(width)); + let height = question_height .saturating_add(options_height) - .saturating_add(4); + .saturating_add(extras); let sections = overlay.layout_sections(Rect::new(0, 0, width, height)); assert_eq!(sections.options_area.height, options_height); } #[test] - fn request_user_input_wrapped_options_snapshot() { + fn desired_height_keeps_spacers_and_preferred_options_visible() { let (tx, _rx) = test_sender(); let overlay = RequestUserInputOverlay::new( request_event( @@ -924,13 +2261,84 @@ mod tests { false, ); - let width = 52u16; + let width = 110u16; + let height = overlay.desired_height(width); + let content_area = menu_surface_inset(Rect::new(0, 0, width, height)); + let sections = overlay.layout_sections(content_area); + let preferred = overlay.options_preferred_height(content_area.width); + + assert_eq!(sections.options_area.height, preferred); + let question_bottom = sections.question_area.y + sections.question_area.height; + let options_bottom = sections.options_area.y + sections.options_area.height; + let spacer_after_question = sections.options_area.y.saturating_sub(question_bottom); + let spacer_after_options = sections.notes_area.y.saturating_sub(options_bottom); + assert_eq!(spacer_after_question, 1); + assert_eq!(spacer_after_options, 1); + } + + #[test] + fn footer_wraps_tips_without_splitting_individual_tips() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + + let width = 36u16; + let lines = overlay.footer_tip_lines(width); + assert!(lines.len() > 1); + let separator_width = UnicodeWidthStr::width(TIP_SEPARATOR); + for tips in lines { + let used = tips.iter().enumerate().fold(0usize, |acc, (idx, tip)| { + let tip_width = UnicodeWidthStr::width(tip.text.as_str()).min(width as usize); + let extra = if idx == 0 { + tip_width + } else { + separator_width.saturating_add(tip_width) + }; + acc.saturating_add(extra) + }); + assert!(used <= width as usize); + } + } + + #[test] + fn request_user_input_wrapped_options_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![question_with_wrapped_options("q1", "Next Step")], + ), + tx, + true, + false, + false, + ); + + { + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + } + + let width = 110u16; let question_height = overlay.wrapped_question_lines(width).len() as u16; let options_height = overlay.options_required_height(width); let height = 1u16 .saturating_add(question_height) .saturating_add(options_height) - .saturating_add(4); + .saturating_add(8); let area = Rect::new(0, 0, width, height); insta::assert_snapshot!( "request_user_input_wrapped_options", @@ -938,6 +2346,34 @@ mod tests { ); } + #[test] + fn request_user_input_footer_wrap_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Pick one"), + question_with_options("q2", "Pick two"), + ], + ), + tx, + true, + false, + false, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(1); + + let width = 52u16; + let height = overlay.desired_height(width); + let area = Rect::new(0, 0, width, height); + insta::assert_snapshot!( + "request_user_input_footer_wrap", + render_snapshot(&overlay, area) + ); + } + #[test] fn request_user_input_scroll_options_snapshot() { let (tx, _rx) = test_sender(); @@ -980,16 +2416,66 @@ mod tests { ); { let answer = overlay.current_answer_mut().expect("answer missing"); - answer.option_state.selected_idx = Some(3); - answer.selected = Some(3); + answer.options_state.selected_idx = Some(3); } - let area = Rect::new(0, 0, 68, 10); + let area = Rect::new(0, 0, 120, 12); insta::assert_snapshot!( "request_user_input_scrolling_options", render_snapshot(&overlay, area) ); } + #[test] + fn request_user_input_hidden_options_footer_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![RequestUserInputQuestion { + id: "q1".to_string(), + header: "Next Step".to_string(), + question: "What would you like to do next?".to_string(), + is_other: false, + options: Some(vec![ + RequestUserInputQuestionOption { + label: "Discuss a code change (Recommended)".to_string(), + description: "Walk through a plan and edit code together.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Run tests".to_string(), + description: "Pick a crate and run its tests.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Review a diff".to_string(), + description: "Summarize or review current changes.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Refactor".to_string(), + description: "Tighten structure and remove dead code.".to_string(), + }, + RequestUserInputQuestionOption { + label: "Ship it".to_string(), + description: "Finalize and open a PR.".to_string(), + }, + ]), + }], + ), + tx, + true, + false, + false, + ); + { + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(3); + } + let area = Rect::new(0, 0, 80, 10); + insta::assert_snapshot!( + "request_user_input_hidden_options_footer", + render_snapshot(&overlay, area) + ); + } + #[test] fn request_user_input_freeform_snapshot() { let (tx, _rx) = test_sender(); @@ -1000,13 +2486,86 @@ mod tests { false, false, ); - let area = Rect::new(0, 0, 64, 10); + let area = Rect::new(0, 0, 120, 10); insta::assert_snapshot!( "request_user_input_freeform", render_snapshot(&overlay, area) ); } + #[test] + fn request_user_input_multi_question_first_snapshot() { + let (tx, _rx) = test_sender(); + let overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Area"), + question_without_options("q2", "Goal"), + ], + ), + tx, + true, + false, + false, + ); + let area = Rect::new(0, 0, 120, 15); + insta::assert_snapshot!( + "request_user_input_multi_question_first", + render_snapshot(&overlay, area) + ); + } + + #[test] + fn request_user_input_multi_question_last_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Area"), + question_without_options("q2", "Goal"), + ], + ), + tx, + true, + false, + false, + ); + overlay.move_question(true); + let area = Rect::new(0, 0, 120, 12); + insta::assert_snapshot!( + "request_user_input_multi_question_last", + render_snapshot(&overlay, area) + ); + } + + #[test] + fn request_user_input_unanswered_confirmation_snapshot() { + let (tx, _rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "Area"), + question_without_options("q2", "Goal"), + ], + ), + tx, + true, + false, + false, + ); + + overlay.open_unanswered_confirmation(); + + let area = Rect::new(0, 0, 80, 12); + insta::assert_snapshot!( + "request_user_input_unanswered_confirmation", + render_snapshot(&overlay, area) + ); + } + #[test] fn options_scroll_while_editing_notes() { let (tx, _rx) = test_sender(); @@ -1017,6 +2576,7 @@ mod tests { false, false, ); + overlay.select_current_option(false); overlay.focus = Focus::Notes; overlay .composer @@ -1026,6 +2586,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Down)); let answer = overlay.current_answer().expect("answer missing"); - assert_eq!(answer.selected, Some(1)); + assert_eq!(answer.options_state.selected_idx, Some(1)); + assert!(!answer.answer_committed); } } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/render.rs b/codex-rs/tui/src/bottom_pane/request_user_input/render.rs index b64f704340..b7433f8a6d 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/render.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/render.rs @@ -1,44 +1,107 @@ -use crossterm::event::KeyCode; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::widgets::Widget; +use std::borrow::Cow; +use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; +use crate::bottom_pane::popup_consts::standard_popup_hint_line; +use crate::bottom_pane::scroll_state::ScrollState; +use crate::bottom_pane::selection_popup_common::measure_rows_height; use crate::bottom_pane::selection_popup_common::menu_surface_inset; use crate::bottom_pane::selection_popup_common::menu_surface_padding_height; use crate::bottom_pane::selection_popup_common::render_menu_surface; use crate::bottom_pane::selection_popup_common::render_rows; -use crate::key_hint; +use crate::bottom_pane::selection_popup_common::wrap_styled_line; use crate::render::renderable::Renderable; +use super::DESIRED_SPACERS_BETWEEN_SECTIONS; use super::RequestUserInputOverlay; +use super::TIP_SEPARATOR; + +const MIN_OVERLAY_HEIGHT: usize = 8; +const PROGRESS_ROW_HEIGHT: usize = 1; +const SPACER_ROWS_WITH_NOTES: usize = 1; +const SPACER_ROWS_NO_OPTIONS: usize = 0; + +struct UnansweredConfirmationData { + title_line: Line<'static>, + subtitle_line: Line<'static>, + hint_line: Line<'static>, + rows: Vec, + state: ScrollState, +} + +struct UnansweredConfirmationLayout { + header_lines: Vec>, + hint_lines: Vec>, + rows: Vec, + state: ScrollState, +} + +fn line_to_owned(line: Line<'_>) -> Line<'static> { + Line { + style: line.style, + alignment: line.alignment, + spans: line + .spans + .into_iter() + .map(|span| Span { + style: span.style, + content: Cow::Owned(span.content.into_owned()), + }) + .collect(), + } +} impl Renderable for RequestUserInputOverlay { fn desired_height(&self, width: u16) -> u16 { + if self.confirm_unanswered_active() { + return self.unanswered_confirmation_height(width); + } let outer = Rect::new(0, 0, width, u16::MAX); let inner = menu_surface_inset(outer); let inner_width = inner.width.max(1); + let has_options = self.has_options(); let question_height = self.wrapped_question_lines(inner_width).len(); - let options_height = self.options_required_height(inner_width) as usize; - let notes_height = self.notes_input_height(inner_width) as usize; - let footer_height = if self.unanswered_count() > 0 { 2 } else { 1 }; + let options_height = if has_options { + self.options_preferred_height(inner_width) as usize + } else { + 0 + }; + let notes_visible = !has_options || self.notes_ui_visible(); + let notes_height = if notes_visible { + self.notes_input_height(inner_width) as usize + } else { + 0 + }; + // When notes are visible, the composer already separates options from the footer. + // Without notes, we keep extra spacing so the footer hints don't crowd the options. + let spacer_rows = if has_options { + if notes_visible { + SPACER_ROWS_WITH_NOTES + } else { + DESIRED_SPACERS_BETWEEN_SECTIONS as usize + } + } else { + SPACER_ROWS_NO_OPTIONS + }; + let footer_height = self.footer_required_height(inner_width) as usize; - // Tight minimum height: progress + header + question + (optional) titles/options + // Tight minimum height: progress + question + (optional) titles/options // + notes composer + footer + menu padding. let mut height = question_height .saturating_add(options_height) + .saturating_add(spacer_rows) .saturating_add(notes_height) .saturating_add(footer_height) - .saturating_add(2); // progress + header - if self.has_options() { - height = height - .saturating_add(1) // answer title - .saturating_add(1); // notes title - } + .saturating_add(PROGRESS_ROW_HEIGHT); // progress height = height.saturating_add(menu_surface_padding_height() as usize); - height.max(8) as u16 + height.max(MIN_OVERLAY_HEIGHT) as u16 } fn render(&self, area: Rect, buf: &mut Buffer) { @@ -51,11 +114,145 @@ impl Renderable for RequestUserInputOverlay { } impl RequestUserInputOverlay { + fn unanswered_confirmation_data(&self) -> UnansweredConfirmationData { + let unanswered = self.unanswered_question_count(); + let subtitle = format!( + "{unanswered} unanswered question{}", + if unanswered == 1 { "" } else { "s" } + ); + UnansweredConfirmationData { + title_line: Line::from(super::UNANSWERED_CONFIRM_TITLE.bold()), + subtitle_line: Line::from(subtitle.dim()), + hint_line: standard_popup_hint_line(), + rows: self.unanswered_confirmation_rows(), + state: self.confirm_unanswered.unwrap_or_default(), + } + } + + fn unanswered_confirmation_layout(&self, width: u16) -> UnansweredConfirmationLayout { + let data = self.unanswered_confirmation_data(); + let content_width = width.max(1); + let mut header_lines = wrap_styled_line(&data.title_line, content_width); + let mut subtitle_lines = wrap_styled_line(&data.subtitle_line, content_width); + header_lines.append(&mut subtitle_lines); + let header_lines = header_lines.into_iter().map(line_to_owned).collect(); + let hint_lines = wrap_styled_line(&data.hint_line, content_width) + .into_iter() + .map(line_to_owned) + .collect(); + UnansweredConfirmationLayout { + header_lines, + hint_lines, + rows: data.rows, + state: data.state, + } + } + + fn unanswered_confirmation_height(&self, width: u16) -> u16 { + let outer = Rect::new(0, 0, width, u16::MAX); + let inner = menu_surface_inset(outer); + let inner_width = inner.width.max(1); + let layout = self.unanswered_confirmation_layout(inner_width); + let rows_height = measure_rows_height( + &layout.rows, + &layout.state, + layout.rows.len().max(1), + inner_width.max(1), + ); + let height = layout.header_lines.len() as u16 + + 1 + + rows_height + + 1 + + layout.hint_lines.len() as u16 + + menu_surface_padding_height(); + height.max(MIN_OVERLAY_HEIGHT as u16) + } + + fn render_unanswered_confirmation(&self, area: Rect, buf: &mut Buffer) { + let content_area = render_menu_surface(area, buf); + if content_area.width == 0 || content_area.height == 0 { + return; + } + let width = content_area.width.max(1); + let layout = self.unanswered_confirmation_layout(width); + + let mut cursor_y = content_area.y; + for line in layout.header_lines { + if cursor_y >= content_area.y + content_area.height { + return; + } + Paragraph::new(line).render( + Rect { + x: content_area.x, + y: cursor_y, + width: content_area.width, + height: 1, + }, + buf, + ); + cursor_y = cursor_y.saturating_add(1); + } + + if cursor_y < content_area.y + content_area.height { + cursor_y = cursor_y.saturating_add(1); + } + + let remaining = content_area + .height + .saturating_sub(cursor_y.saturating_sub(content_area.y)); + if remaining == 0 { + return; + } + + let hint_height = layout.hint_lines.len() as u16; + let spacer_before_hint = u16::from(remaining > hint_height); + let rows_height = remaining.saturating_sub(hint_height + spacer_before_hint); + + let rows_area = Rect { + x: content_area.x, + y: cursor_y, + width: content_area.width, + height: rows_height, + }; + render_rows( + rows_area, + buf, + &layout.rows, + &layout.state, + layout.rows.len().max(1), + "No choices", + ); + + cursor_y = cursor_y.saturating_add(rows_height); + if spacer_before_hint > 0 { + cursor_y = cursor_y.saturating_add(1); + } + for (offset, line) in layout.hint_lines.into_iter().enumerate() { + let y = cursor_y.saturating_add(offset as u16); + if y >= content_area.y + content_area.height { + break; + } + Paragraph::new(line).render( + Rect { + x: content_area.x, + y, + width: content_area.width, + height: 1, + }, + buf, + ); + } + } + /// Render the full request-user-input overlay. pub(super) fn render_ui(&self, area: Rect, buf: &mut Buffer) { if area.width == 0 || area.height == 0 { return; } + if self.confirm_unanswered_active() { + self.render_unanswered_confirmation(area, buf); + return; + } // Paint the same menu surface used by other bottom-pane overlays and // then render the overlay content inside its inset area. let content_area = render_menu_surface(area, buf); @@ -63,34 +260,40 @@ impl RequestUserInputOverlay { return; } let sections = self.layout_sections(content_area); + let notes_visible = self.notes_ui_visible(); + let unanswered = self.unanswered_count(); // Progress header keeps the user oriented across multiple questions. let progress_line = if self.question_count() > 0 { let idx = self.current_index() + 1; let total = self.question_count(); - Line::from(format!("Question {idx}/{total}").dim()) + let base = format!("Question {idx}/{total}"); + if unanswered > 0 { + Line::from(format!("{base} ({unanswered} unanswered)").dim()) + } else { + Line::from(base.dim()) + } } else { Line::from("No questions".dim()) }; Paragraph::new(progress_line).render(sections.progress_area, buf); - // Question title and wrapped prompt text. - let question_header = self.current_question().map(|q| q.header.clone()); - let header_line = if let Some(header) = question_header { - Line::from(header.bold()) - } else { - Line::from("No questions".dim()) - }; - Paragraph::new(header_line).render(sections.header_area, buf); - + // Question prompt text. let question_y = sections.question_area.y; + let answered = + self.is_question_answered(self.current_index(), &self.composer.current_text()); for (offset, line) in sections.question_lines.iter().enumerate() { if question_y.saturating_add(offset as u16) >= sections.question_area.y + sections.question_area.height { break; } - Paragraph::new(Line::from(line.clone())).render( + let question_line = if answered { + Line::from(line.clone()) + } else { + Line::from(line.clone()).cyan() + }; + Paragraph::new(question_line).render( Rect { x: sections.question_area.x, y: question_y.saturating_add(offset as u16), @@ -101,62 +304,30 @@ impl RequestUserInputOverlay { ); } - if sections.answer_title_area.height > 0 { - let answer_label = "Answer"; - let answer_title = if self.focus_is_options() || self.focus_is_notes_without_options() { - answer_label.cyan().bold() - } else { - answer_label.dim() - }; - Paragraph::new(Line::from(answer_title)).render(sections.answer_title_area, buf); - } - // Build rows with selection markers for the shared selection renderer. let option_rows = self.option_rows(); if self.has_options() { - let mut option_state = self + let mut options_state = self .current_answer() - .map(|answer| answer.option_state) + .map(|answer| answer.options_state) .unwrap_or_default(); if sections.options_area.height > 0 { // Ensure the selected option is visible in the scroll window. - option_state + options_state .ensure_visible(option_rows.len(), sections.options_area.height as usize); render_rows( sections.options_area, buf, &option_rows, - &option_state, + &options_state, option_rows.len().max(1), "No options", ); } } - if sections.notes_title_area.height > 0 { - let notes_label = if self.has_options() - && self - .current_answer() - .is_some_and(|answer| answer.selected.is_some()) - { - if let Some(label) = self.current_option_label() { - format!("Notes for {label} (optional)") - } else { - "Notes (optional)".to_string() - } - } else { - "Notes (optional)".to_string() - }; - let notes_title = if self.focus_is_notes() { - notes_label.as_str().cyan().bold() - } else { - notes_label.as_str().dim() - }; - Paragraph::new(Line::from(notes_title)).render(sections.notes_title_area, buf); - } - - if sections.notes_area.height > 0 { + if notes_visible && sections.notes_area.height > 0 { self.render_notes_input(sections.notes_area, buf); } @@ -164,69 +335,68 @@ impl RequestUserInputOverlay { .notes_area .y .saturating_add(sections.notes_area.height); - if sections.footer_lines == 2 { - // Status line for unanswered count when any question is empty. - let warning = format!( - "Unanswered: {} | Will submit as skipped", - self.unanswered_count() - ); - Paragraph::new(Line::from(warning.dim())).render( - Rect { - x: content_area.x, - y: footer_y, - width: content_area.width, - height: 1, - }, - buf, - ); + let footer_area = Rect { + x: content_area.x, + y: footer_y, + width: content_area.width, + height: sections.footer_lines, + }; + if footer_area.height == 0 { + return; } - let hint_y = footer_y.saturating_add(sections.footer_lines.saturating_sub(1)); - // Footer hints (selection index + navigation keys). - let mut hint_spans = Vec::new(); - if self.has_options() { - let options_len = self.options_len(); - let option_index = self.selected_option_index().map_or(0, |idx| idx + 1); - hint_spans.extend(vec![ - format!("Option {option_index} of {options_len}").into(), - " | ".into(), - ]); - } - hint_spans.extend(vec![ - key_hint::plain(KeyCode::Up).into(), - "/".into(), - key_hint::plain(KeyCode::Down).into(), - " scroll | ".into(), - key_hint::plain(KeyCode::Enter).into(), - " next question | ".into(), - ]); - if self.question_count() > 1 { - hint_spans.extend(vec![ - key_hint::plain(KeyCode::PageUp).into(), - " prev | ".into(), - key_hint::plain(KeyCode::PageDown).into(), - " next | ".into(), - ]); - } - hint_spans.extend(vec![ - key_hint::plain(KeyCode::Esc).into(), - " interrupt".into(), - ]); - Paragraph::new(Line::from(hint_spans).dim()).render( - Rect { - x: content_area.x, - y: hint_y, - width: content_area.width, + let options_hidden = self.has_options() + && sections.options_area.height > 0 + && self.options_required_height(content_area.width) > sections.options_area.height; + let option_tip = if options_hidden { + let selected = self.selected_option_index().unwrap_or(0).saturating_add(1); + let total = self.options_len(); + Some(super::FooterTip::new(format!("option {selected}/{total}"))) + } else { + None + }; + let tip_lines = self.footer_tip_lines_with_prefix(footer_area.width, option_tip); + for (row_idx, tips) in tip_lines + .into_iter() + .take(footer_area.height as usize) + .enumerate() + { + let mut spans = Vec::new(); + for (tip_idx, tip) in tips.into_iter().enumerate() { + if tip_idx > 0 { + spans.push(TIP_SEPARATOR.into()); + } + if tip.highlight { + spans.push(tip.text.cyan().bold().not_dim()); + } else { + spans.push(tip.text.into()); + } + } + let line = Line::from(spans).dim(); + let line = truncate_line_word_boundary_with_ellipsis(line, footer_area.width as usize); + let row_area = Rect { + x: footer_area.x, + y: footer_area.y.saturating_add(row_idx as u16), + width: footer_area.width, height: 1, - }, - buf, - ); + }; + Paragraph::new(line).render(row_area, buf); + } } /// Return the cursor position when editing notes, if visible. pub(super) fn cursor_pos_impl(&self, area: Rect) -> Option<(u16, u16)> { + if self.confirm_unanswered_active() { + return None; + } + let has_options = self.has_options(); + let notes_visible = self.notes_ui_visible(); + if !self.focus_is_notes() { return None; } + if has_options && !notes_visible { + return None; + } let content_area = menu_surface_inset(area); if content_area.width == 0 || content_area.height == 0 { return None; @@ -246,16 +416,118 @@ impl RequestUserInputOverlay { } self.composer.render(area, buf); } - - fn focus_is_options(&self) -> bool { - matches!(self.focus, super::Focus::Options) - } - - fn focus_is_notes(&self) -> bool { - matches!(self.focus, super::Focus::Notes) - } - - fn focus_is_notes_without_options(&self) -> bool { - !self.has_options() && self.focus_is_notes() - } +} + +fn line_width(line: &Line<'_>) -> usize { + line.iter() + .map(|span| UnicodeWidthStr::width(span.content.as_ref())) + .sum() +} + +/// Truncate a styled line to `max_width`, preferring a word boundary, and append an ellipsis. +/// +/// This walks spans character-by-character, tracking the last width-safe position and the last +/// whitespace boundary within the available width (excluding the ellipsis width). If the line +/// overflows, it truncates at the last word boundary when possible (falling back to the last +/// fitting character), trims trailing whitespace, then appends an ellipsis styled to match the +/// last visible span (or the line style if nothing was kept). +fn truncate_line_word_boundary_with_ellipsis( + line: Line<'static>, + max_width: usize, +) -> Line<'static> { + if max_width == 0 { + return Line::from(Vec::>::new()); + } + + if line_width(&line) <= max_width { + return line; + } + + let ellipsis = "…"; + let ellipsis_width = UnicodeWidthStr::width(ellipsis); + if ellipsis_width >= max_width { + return Line::from(ellipsis); + } + let limit = max_width.saturating_sub(ellipsis_width); + + #[derive(Clone, Copy)] + struct BreakPoint { + span_idx: usize, + byte_end: usize, + } + + // Track display width as we scan, along with the best "cut here" positions. + let mut used = 0usize; + let mut last_fit: Option = None; + let mut last_word_break: Option = None; + let mut overflowed = false; + + 'outer: for (span_idx, span) in line.spans.iter().enumerate() { + let text = span.content.as_ref(); + for (byte_idx, ch) in text.char_indices() { + let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); + if used.saturating_add(ch_width) > limit { + overflowed = true; + break 'outer; + } + used = used.saturating_add(ch_width); + let bp = BreakPoint { + span_idx, + byte_end: byte_idx + ch.len_utf8(), + }; + last_fit = Some(bp); + if ch.is_whitespace() { + last_word_break = Some(bp); + } + } + } + + // If we never overflowed, the original line already fits. + if !overflowed { + return line; + } + + // Prefer breaking on whitespace; otherwise fall back to the last fitting character. + let chosen_break = last_word_break.or(last_fit); + let Some(chosen_break) = chosen_break else { + return Line::from(ellipsis); + }; + + let line_style = line.style; + let mut spans_out: Vec> = Vec::new(); + for (idx, span) in line.spans.into_iter().enumerate() { + if idx < chosen_break.span_idx { + spans_out.push(span); + continue; + } + if idx == chosen_break.span_idx { + let text = span.content.into_owned(); + let truncated = text[..chosen_break.byte_end].to_string(); + if !truncated.is_empty() { + spans_out.push(Span::styled(truncated, span.style)); + } + } + break; + } + + while let Some(last) = spans_out.last_mut() { + let trimmed = last + .content + .trim_end_matches(char::is_whitespace) + .to_string(); + if trimmed.is_empty() { + spans_out.pop(); + } else { + last.content = trimmed.into(); + break; + } + } + + let ellipsis_style = spans_out + .last() + .map(|span| span.style) + .unwrap_or(line_style); + spans_out.push(Span::styled(ellipsis, ellipsis_style)); + + Line::from(spans_out).style(line_style) } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_footer_wrap.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_footer_wrap.snap new file mode 100644 index 0000000000..3fd7194648 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_footer_wrap.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 1/2 (2 unanswered) + Choose an option. + + 1. Option 1 First choice. + › 2. Option 2 Second choice. + 3. Option 3 Third choice. + + tab to add notes | enter to submit answer + ctrl + n next question | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform.snap index dd5ced6fca..3ae7b9d624 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform.snap @@ -2,12 +2,12 @@ source: tui/src/bottom_pane/request_user_input/mod.rs expression: "render_snapshot(&overlay, area)" --- - - Question 1/1 - Goal - Share details. - - › Type your answer (optional) - - Unanswered: 1 | Will submit as skipped - ↑/↓ scroll | enter next question | esc interrupt + + Question 1/1 (1 unanswered) + Share details. + + › Type your answer (optional) + + + + enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_hidden_options_footer.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_hidden_options_footer.snap new file mode 100644 index 0000000000..d643647f79 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_hidden_options_footer.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 1/1 (1 unanswered) + What would you like to do next? + + 2. Run tests Pick a crate and run its tests. + 3. Review a diff Summarize or review current changes. + › 4. Refactor Tighten structure and remove dead code. + + option 4/5 | tab to add notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_first.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_first.snap new file mode 100644 index 0000000000..536e34dbba --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_first.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 1/2 (2 unanswered) + Choose an option. + + › 1. Option 1 First choice. + 2. Option 2 Second choice. + 3. Option 3 Third choice. + + tab to add notes | enter to submit answer | ctrl + n next question | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_last.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_last.snap new file mode 100644 index 0000000000..95507c3358 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_multi_question_last.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 2/2 (2 unanswered) + Share details. + + › Type your answer (optional) + + + + + + enter to submit all | ctrl + n first question | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options.snap index 37ed036478..c93576246d 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options.snap @@ -2,18 +2,12 @@ source: tui/src/bottom_pane/request_user_input/mod.rs expression: "render_snapshot(&overlay, area)" --- - - Question 1/1 - Area - Choose an option. - Answer - (x) Option 1 First choice. - ( ) Option 2 Second choice. - ( ) Option 3 Third choice. - Notes for Option 1 (optional) - - › Add notes (optional) - - - - Option 1 of 3 | ↑/↓ scroll | enter next question | esc inter + + Question 1/1 (1 unanswered) + Choose an option. + + › 1. Option 1 First choice. + 2. Option 2 Second choice. + 3. Option 3 Third choice. + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options_notes_visible.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options_notes_visible.snap new file mode 100644 index 0000000000..d9d219b627 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_options_notes_visible.snap @@ -0,0 +1,19 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 1/1 (1 unanswered) + Choose an option. + + › 1. Option 1 First choice. + 2. Option 2 Second choice. + 3. Option 3 Third choice. + + › Add notes + + + + + + tab to clear notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_scrolling_options.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_scrolling_options.snap index ace8eb4537..2e8d120e44 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_scrolling_options.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_scrolling_options.snap @@ -2,13 +2,14 @@ source: tui/src/bottom_pane/request_user_input/mod.rs expression: "render_snapshot(&overlay, area)" --- - - Next Step - What would you like to do next? - ( ) Discuss a code change (Recommended) Walk through a plan and - edit code together. - ( ) Run tests Pick a crate and run - its tests. - ( ) Review a diff Summarize or review - current changes. - Option 4 of 5 | ↑/↓ scroll | enter next question | esc interrupt + + Question 1/1 (1 unanswered) + What would you like to do next? + + 1. Discuss a code change (Recommended) Walk through a plan and edit code together. + 2. Run tests Pick a crate and run its tests. + 3. Review a diff Summarize or review current changes. + › 4. Refactor Tighten structure and remove dead code. + 5. Ship it Finalize and open a PR. + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_tight_height.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_tight_height.snap index e8cd2bd22e..c93576246d 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_tight_height.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_tight_height.snap @@ -2,11 +2,12 @@ source: tui/src/bottom_pane/request_user_input/mod.rs expression: "render_snapshot(&overlay, area)" --- - - Area - Choose an option. - (x) Option 1 First choice. - ( ) Option 2 Second choice. - ( ) Option 3 Third choice. - - Option 1 of 3 | ↑/↓ scroll | enter next question | esc i + + Question 1/1 (1 unanswered) + Choose an option. + + › 1. Option 1 First choice. + 2. Option 2 Second choice. + 3. Option 3 Third choice. + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_unanswered_confirmation.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_unanswered_confirmation.snap new file mode 100644 index 0000000000..dd689c7267 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_unanswered_confirmation.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Submit with unanswered questions? + 2 unanswered questions + + › 1. Proceed Submit with 2 unanswered questions. + 2. Go back Return to the first unanswered question. + + + + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_wrapped_options.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_wrapped_options.snap index 4ae9dd048f..71d32c5abf 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_wrapped_options.snap +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_wrapped_options.snap @@ -2,20 +2,12 @@ source: tui/src/bottom_pane/request_user_input/mod.rs expression: "render_snapshot(&overlay, area)" --- - - Next Step - Choose the next step for this task. - (x) Discuss a code change Walk through a plan, - then implement it - together with careful - checks. - ( ) Run targeted tests Pick the most - relevant crate and - validate the current - behavior first. - ( ) Review the diff Summarize the changes - and highlight the - most important risks - and gaps. - - Option 1 of 3 | ↑/↓ scroll | enter next question + + Question 1/1 (1 unanswered) + Choose the next step for this task. + + › 1. Discuss a code change Walk through a plan, then implement it together with careful checks. + 2. Run targeted tests Pick the most relevant crate and validate the current behavior first. + 3. Review the diff Summarize the changes and highlight the most important risks and gaps. + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index 55765a00da..e0707a4228 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -8,20 +8,12 @@ use codex_common::fuzzy_match::fuzzy_match; use crate::slash_command::SlashCommand; use crate::slash_command::built_in_slash_commands; -/// Whether the Windows degraded-sandbox elevation flow is currently allowed. -pub(crate) fn windows_degraded_sandbox_active() -> bool { - cfg!(target_os = "windows") - && codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled() -} - /// Return the built-ins that should be visible/usable for the current input. pub(crate) fn builtins_for_input( collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> Vec<(&'static str, SlashCommand)> { - let allow_elevate_sandbox = windows_degraded_sandbox_active(); built_in_slash_commands() .into_iter() .filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox) @@ -35,11 +27,16 @@ pub(crate) fn find_builtin_command( name: &str, collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> Option { - builtins_for_input(collaboration_modes_enabled, personality_command_enabled) - .into_iter() - .find(|(command_name, _)| *command_name == name) - .map(|(_, cmd)| cmd) + builtins_for_input( + collaboration_modes_enabled, + personality_command_enabled, + allow_elevate_sandbox, + ) + .into_iter() + .find(|(command_name, _)| *command_name == name) + .map(|(_, cmd)| cmd) } /// Whether any visible built-in fuzzily matches the provided prefix. @@ -47,8 +44,13 @@ pub(crate) fn has_builtin_prefix( name: &str, collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> bool { - builtins_for_input(collaboration_modes_enabled, personality_command_enabled) - .into_iter() - .any(|(command_name, _)| fuzzy_match(command_name, name).is_some()) + builtins_for_input( + collaboration_modes_enabled, + personality_command_enabled, + allow_elevate_sandbox, + ) + .into_iter() + .any(|(command_name, _)| fuzzy_match(command_name, name).is_some()) } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index e4cc9ffefd..0b88e19a22 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index 53e0aee4cf..47c97c74d2 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap new file mode 100644 index 0000000000..90a7b12e4a --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ? for shortcuts · Code mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap new file mode 100644 index 0000000000..cfc93be1e6 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" Code mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap new file mode 100644 index 0000000000..9901a8fbbf --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" Code mode (shift+tab to cycle) " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap new file mode 100644 index 0000000000..98c7c33889 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anythin " +" " +" " +" " +" " +" " +" " +" Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap new file mode 100644 index 0000000000..cfb47fa9d7 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue message · Code mode 98% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap new file mode 100644 index 0000000000..0a91488b58 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue message · Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap new file mode 100644 index 0000000000..9adcc6e059 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap new file mode 100644 index 0000000000..0910602616 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue · Code mode 98% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap new file mode 100644 index 0000000000..993dc34035 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue · Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap index 67e616e917..5faacfa64f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap index 3f1adf6291..49eca416c2 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/chat_composer.rs -assertion_line: 2116 expression: terminal.backend() --- " " @@ -11,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap index e46fa0a740..3a5dd7a758 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/chat_composer.rs -assertion_line: 2116 expression: terminal.backend() --- " " @@ -11,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index 6b018021ec..d2f77dbec3 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index 40098faee0..0d16cec0b4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index 498ed76936..8d3f8216db 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " 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 new file mode 100644 index 0000000000..6fdeda07b1 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap new file mode 100644 index 0000000000..71370d83ba --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" tab to queue message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap deleted file mode 100644 index ce36b2ada8..0000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_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/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap deleted file mode 100644 index b9733866d7..0000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/bottom_pane/footer.rs -expression: terminal.backend() ---- -" 100% context left · tab to queue message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap index a77ca5565b..b7ee60704c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 123K used · ? for shortcuts " +" ? for shortcuts 123K used " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap index ed9fea7c88..6266f43d0b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts " +" Plan mode (shift+tab to cycle) " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap index fe5868b87c..9f9be080da 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts Plan mode " +" ? for shortcuts · Plan mode 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap index 7212d6de51..8c32ee50dc 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts Plan mode (shift+tab to cycle) " +" ? for shortcuts · Plan mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap index d05ac90a91..2a81b85576 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 72% context left · ? for shortcuts " +" ? for shortcuts 72% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap index c95a5dc0b3..02804e5735 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap index 5aea415190..47581631c2 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap index 86e3da4573..494883e4c3 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for sh + 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap index e651ec9274..a82f6512cf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap @@ -9,4 +9,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap index 79e1e126eb..136c358055 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap index 12090d09e9..b714c69d88 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap @@ -11,4 +11,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9dc1bc4bfe..bd1014b2f4 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -88,6 +88,8 @@ use codex_core::protocol::WarningEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; use codex_core::skills::model::SkillMetadata; +#[cfg(target_os = "windows")] +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; @@ -97,6 +99,8 @@ use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::Settings; +#[cfg(target_os = "windows")] +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::request_user_input::RequestUserInputEvent; @@ -2057,6 +2061,14 @@ impl ChatWidget { widget.config.features.enabled(Feature::CollaborationModes), ); widget.sync_personality_command_enabled(); + #[cfg(target_os = "windows")] + widget.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&widget.config), + WindowsSandboxLevel::RestrictedToken + ), + ); widget.update_collaboration_mode_indicator(); widget @@ -2308,6 +2320,14 @@ impl ChatWidget { widget.config.features.enabled(Feature::CollaborationModes), ); widget.sync_personality_command_enabled(); + #[cfg(target_os = "windows")] + widget.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&widget.config), + WindowsSandboxLevel::RestrictedToken + ), + ); widget.update_collaboration_mode_indicator(); widget @@ -2563,9 +2583,9 @@ impl ChatWidget { SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox() - .is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_degraded_sandbox_enabled = + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); if !windows_degraded_sandbox_enabled || !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { @@ -3340,6 +3360,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(switch_model.clone()), effort: Some(Some(default_effort)), summary: None, @@ -3462,6 +3483,7 @@ impl ChatWidget { effort: None, summary: None, collaboration_mode: None, + windows_sandbox_level: None, personality: Some(personality), })); tx.send(AppEvent::UpdatePersonality(personality)); @@ -3731,6 +3753,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model_for_action.clone()), effort: Some(effort_for_action), summary: None, @@ -3904,6 +3927,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model.clone()), effort: Some(effort), summary: None, @@ -3944,8 +3968,10 @@ impl ChatWidget { let presets: Vec = builtin_approval_presets(); #[cfg(target_os = "windows")] - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + #[cfg(target_os = "windows")] + let windows_degraded_sandbox_enabled = + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); #[cfg(not(target_os = "windows"))] let windows_degraded_sandbox_enabled = false; @@ -3986,7 +4012,9 @@ impl ChatWidget { } else if preset.id == "auto" { #[cfg(target_os = "windows")] { - if codex_core::get_platform_sandbox().is_none() { + if WindowsSandboxLevel::from_config(&self.config) + == WindowsSandboxLevel::Disabled + { let preset_clone = preset.clone(); if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED && codex_core::windows_sandbox::sandbox_setup_is_complete( @@ -4088,6 +4116,7 @@ impl ChatWidget { cwd: None, approval_policy: Some(approval), sandbox_policy: Some(sandbox_clone.clone()), + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -4591,7 +4620,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { if self.config.forced_auto_mode_downgraded_on_windows - && codex_core::get_platform_sandbox().is_none() + && WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled && let Some(preset) = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -4651,7 +4680,7 @@ impl ChatWidget { pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { #[cfg(target_os = "windows")] let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some(); + || WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled; self.config.sandbox_policy.set(policy)?; @@ -4685,6 +4714,19 @@ impl ChatWidget { self.refresh_model_display(); self.request_redraw(); } + #[cfg(target_os = "windows")] + if matches!( + feature, + Feature::WindowsSandbox | Feature::WindowsSandboxElevated + ) { + self.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + ), + ); + } } pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) { diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index d72ca60455..d6893a9e3c 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -15,8 +15,10 @@ use crate::skills_helpers::skill_display_name; use codex_core::protocol::ListSkillsResponseEvent; use codex_core::protocol::SkillMetadata as ProtocolSkillMetadata; use codex_core::protocol::SkillsListEntry; +use codex_core::skills::model::SkillDependencies; use codex_core::skills::model::SkillInterface; use codex_core::skills::model::SkillMetadata; +use codex_core::skills::model::SkillToolDependency; impl ChatWidget { pub(crate) fn open_skills_list(&mut self) { @@ -168,6 +170,23 @@ fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata { brand_color: interface.brand_color, default_prompt: interface.default_prompt, }), + dependencies: skill + .dependencies + .clone() + .map(|dependencies| SkillDependencies { + tools: dependencies + .tools + .into_iter() + .map(|tool| SkillToolDependency { + r#type: tool.r#type, + value: tool.value, + description: tool.description, + transport: tool.transport, + command: tool.command, + url: tool.url, + }) + .collect(), + }), path: skill.path.clone(), scope: skill.scope, } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index b51d759fe7..52779fd840 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -41,4 +41,4 @@ expression: term.backend().vt100().screen().contents() › Summarize recent commits - 100% context left · tab to queue message + tab to queue message 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index 64361e90f9..ebffeb8f53 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -25,4 +25,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap index c6866c1b51..6074ed1f20 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap index 1c02350a6d..ce28175ea6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -19,4 +19,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index 9fbebfb500..3acfd95eec 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 1577 expression: terminal.backend() --- " " @@ -9,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 44aa0340cb..64060aece8 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -92,16 +92,6 @@ use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::unbounded_channel; use toml::Value as TomlValue; -#[cfg(target_os = "windows")] -fn set_windows_sandbox_enabled(enabled: bool) { - codex_core::set_windows_sandbox_enabled(enabled); -} - -#[cfg(target_os = "windows")] -fn set_windows_elevated_sandbox_enabled(enabled: bool) { - codex_core::set_windows_elevated_sandbox_enabled(enabled); -} - async fn test_config() -> Config { // Use base defaults to avoid depending on host state. let codex_home = std::env::temp_dir(); @@ -1759,10 +1749,7 @@ async fn ctrl_c_cleared_prompt_is_recoverable_via_history() { assert!(!chat.bottom_pane.quit_shortcut_hint_visible()); let images = chat.bottom_pane.take_recent_submission_images(); - assert!( - images.is_empty(), - "attachments are not preserved in history recall" - ); + assert_eq!(vec![PathBuf::from("/tmp/preview.png")], images); } #[tokio::test] @@ -3050,16 +3037,9 @@ async fn approvals_selection_popup_snapshot() { async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - let was_sandbox_enabled = codex_core::get_platform_sandbox().is_some(); - let was_elevated_enabled = codex_core::is_windows_elevated_sandbox_enabled(); - chat.config.notices.hide_full_access_warning = None; - chat.config.features.enable(Feature::WindowsSandbox); - chat.config - .features - .disable(Feature::WindowsSandboxElevated); - set_windows_sandbox_enabled(true); - set_windows_elevated_sandbox_enabled(false); + chat.set_feature_enabled(Feature::WindowsSandbox, true); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, false); chat.open_approvals_popup(); @@ -3067,10 +3047,6 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { insta::with_settings!({ snapshot_suffix => "windows_degraded" }, { assert_snapshot!("approvals_selection_popup", popup); }); - - // Avoid leaking sandbox global state into other tests. - set_windows_sandbox_enabled(was_sandbox_enabled); - set_windows_elevated_sandbox_enabled(was_elevated_enabled); } #[tokio::test] @@ -3133,7 +3109,8 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { async fn startup_prompts_for_windows_sandbox_when_agent_requested() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - set_windows_sandbox_enabled(false); + chat.set_feature_enabled(Feature::WindowsSandbox, false); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, false); chat.config.forced_auto_mode_downgraded_on_windows = true; chat.maybe_prompt_windows_sandbox_enable(); @@ -3151,8 +3128,6 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { popup.contains("Stay in"), "expected startup prompt to offer staying in current mode: {popup}" ); - - set_windows_sandbox_enabled(true); } #[tokio::test] diff --git a/codex-rs/tui/src/file_search.rs b/codex-rs/tui/src/file_search.rs index af46512640..88c56f2316 100644 --- a/codex-rs/tui/src/file_search.rs +++ b/codex-rs/tui/src/file_search.rs @@ -1,32 +1,15 @@ -//! Helper that owns the debounce/cancellation logic for `@` file searches. +//! Session-based orchestration for `@` file searches. //! -//! `ChatComposer` publishes *every* change of the `@token` as -//! `AppEvent::StartFileSearch(query)`. -//! This struct receives those events and decides when to actually spawn the -//! expensive search (handled in the main `App` thread). It tries to ensure: -//! -//! - Even when the user types long text quickly, they will start seeing results -//! after a short delay using an early version of what they typed. -//! - At most one search is in-flight at any time. -//! -//! It works as follows: -//! -//! 1. First query starts a debounce timer. -//! 2. While the timer is pending, the latest query from the user is stored. -//! 3. When the timer fires, it is cleared, and a search is done for the most -//! recent query. -//! 4. If there is a in-flight search that is not a prefix of the latest thing -//! the user typed, it is cancelled. +//! `ChatComposer` publishes every change of the `@token` as +//! `AppEvent::StartFileSearch(query)`. This manager owns a single +//! `codex-file-search` session for the current search root, updates the query +//! on every keystroke, and drops the session when the query becomes empty. use codex_file_search as file_search; use std::num::NonZeroUsize; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::thread; -use std::time::Duration; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -34,35 +17,16 @@ use crate::app_event_sender::AppEventSender; const MAX_FILE_SEARCH_RESULTS: NonZeroUsize = NonZeroUsize::new(20).unwrap(); const NUM_FILE_SEARCH_THREADS: NonZeroUsize = NonZeroUsize::new(2).unwrap(); -/// How long to wait after a keystroke before firing the first search when none -/// is currently running. Keeps early queries more meaningful. -const FILE_SEARCH_DEBOUNCE: Duration = Duration::from_millis(100); - -const ACTIVE_SEARCH_COMPLETE_POLL_INTERVAL: Duration = Duration::from_millis(20); - -/// State machine for file-search orchestration. pub(crate) struct FileSearchManager { - /// Unified state guarded by one mutex. state: Arc>, - search_dir: PathBuf, app_tx: AppEventSender, } struct SearchState { - /// Latest query typed by user (updated every keystroke). latest_query: String, - - /// true if a search is currently scheduled. - is_search_scheduled: bool, - - /// If there is an active search, this will be the query being searched. - active_search: Option, -} - -struct ActiveSearch { - query: String, - cancellation_token: Arc, + session: Option, + session_token: usize, } impl FileSearchManager { @@ -70,8 +34,8 @@ impl FileSearchManager { Self { state: Arc::new(Mutex::new(SearchState { latest_query: String::new(), - is_search_scheduled: false, - active_search: None, + session: None, + session_token: 0, })), search_dir, app_tx: tx, @@ -80,120 +44,85 @@ impl FileSearchManager { /// Call whenever the user edits the `@` token. pub fn on_user_query(&self, query: String) { - { - #[expect(clippy::unwrap_used)] - let mut st = self.state.lock().unwrap(); - if query == st.latest_query { - // No change, nothing to do. - return; - } + #[expect(clippy::unwrap_used)] + let mut st = self.state.lock().unwrap(); + if query == st.latest_query { + return; + } + st.latest_query.clear(); + st.latest_query.push_str(&query); - // Update latest query. - st.latest_query.clear(); - st.latest_query.push_str(&query); - - // If there is an in-flight search that is definitely obsolete, - // cancel it now. - if let Some(active_search) = &st.active_search - && !query.starts_with(&active_search.query) - { - active_search - .cancellation_token - .store(true, Ordering::Relaxed); - st.active_search = None; - } - - // Schedule a search to run after debounce. - if !st.is_search_scheduled { - st.is_search_scheduled = true; - } else { - return; - } + if query.is_empty() { + st.session.take(); + return; } - // If we are here, we set `st.is_search_scheduled = true` before - // dropping the lock. This means we are the only thread that can spawn a - // debounce timer. - let state = self.state.clone(); - let search_dir = self.search_dir.clone(); - let tx_clone = self.app_tx.clone(); - thread::spawn(move || { - // Always do a minimum debounce, but then poll until the - // `active_search` is cleared. - thread::sleep(FILE_SEARCH_DEBOUNCE); - loop { - #[expect(clippy::unwrap_used)] - if state.lock().unwrap().active_search.is_none() { - break; - } - thread::sleep(ACTIVE_SEARCH_COMPLETE_POLL_INTERVAL); - } - - // The debounce timer has expired, so start a search using the - // latest query. - let cancellation_token = Arc::new(AtomicBool::new(false)); - let token = cancellation_token.clone(); - let query = { - #[expect(clippy::unwrap_used)] - let mut st = state.lock().unwrap(); - let query = st.latest_query.clone(); - st.is_search_scheduled = false; - st.active_search = Some(ActiveSearch { - query: query.clone(), - cancellation_token: token, - }); - query - }; - - FileSearchManager::spawn_file_search( - query, - search_dir, - tx_clone, - cancellation_token, - state, - ); - }); + if st.session.is_none() { + self.start_session_locked(&mut st); + } + if let Some(session) = st.session.as_ref() { + session.update_query(&query); + } } - fn spawn_file_search( - query: String, - search_dir: PathBuf, - tx: AppEventSender, - cancellation_token: Arc, - search_state: Arc>, - ) { - let compute_indices = true; - std::thread::spawn(move || { - let matches = file_search::run( - &query, - MAX_FILE_SEARCH_RESULTS, - &search_dir, - Vec::new(), - NUM_FILE_SEARCH_THREADS, - cancellation_token.clone(), - compute_indices, - true, - ) - .map(|res| res.matches) - .unwrap_or_default(); - - let is_cancelled = cancellation_token.load(Ordering::Relaxed); - if !is_cancelled { - tx.send(AppEvent::FileSearchResult { query, matches }); + fn start_session_locked(&self, st: &mut SearchState) { + st.session_token = st.session_token.wrapping_add(1); + let session_token = st.session_token; + let reporter = Arc::new(TuiSessionReporter { + state: self.state.clone(), + app_tx: self.app_tx.clone(), + session_token, + }); + let session = file_search::create_session( + &self.search_dir, + file_search::SessionOptions { + limit: MAX_FILE_SEARCH_RESULTS, + exclude: Vec::new(), + threads: NUM_FILE_SEARCH_THREADS, + compute_indices: true, + respect_gitignore: true, + }, + reporter, + ); + match session { + Ok(session) => st.session = Some(session), + Err(err) => { + tracing::warn!("file search session failed to start: {err}"); + st.session = None; } + } + } +} - // Reset the active search state. Do a pointer comparison to verify - // that we are clearing the ActiveSearch that corresponds to the - // cancellation token we were given. - { - #[expect(clippy::unwrap_used)] - let mut st = search_state.lock().unwrap(); - if let Some(active_search) = &st.active_search - && Arc::ptr_eq(&active_search.cancellation_token, &cancellation_token) - { - st.active_search = None; - } - } +struct TuiSessionReporter { + state: Arc>, + app_tx: AppEventSender, + session_token: usize, +} + +impl TuiSessionReporter { + fn send_snapshot(&self, snapshot: &file_search::FileSearchSnapshot) { + #[expect(clippy::unwrap_used)] + let st = self.state.lock().unwrap(); + if st.session_token != self.session_token + || st.latest_query.is_empty() + || snapshot.query.is_empty() + { + return; + } + let query = snapshot.query.clone(); + drop(st); + self.app_tx.send(AppEvent::FileSearchResult { + query, + matches: snapshot.matches.clone(), }); } } + +impl file_search::SessionReporter for TuiSessionReporter { + fn on_update(&self, snapshot: &file_search::FileSearchSnapshot) { + self.send_snapshot(snapshot); + } + + fn on_complete(&self) {} +} diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 02a148df4c..687d18fa6a 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1429,44 +1429,62 @@ pub(crate) fn new_web_search_call( cell } -/// If the first content is an image, return a new cell with the image. -/// TODO(rgwood-dd): Handle images properly even if they're not the first result. +/// Returns an additional history cell if an MCP tool result includes a decodable image. +/// +/// This intentionally returns at most one cell: the first image in `CallToolResult.content` that +/// successfully base64-decodes and parses as an image. This is used as a lightweight “image output +/// exists” affordance separate from the main MCP tool call cell. +/// +/// Manual testing tip: +/// - Run the rmcp stdio test server (`codex-rs/rmcp-client/src/bin/test_stdio_server.rs`) and +/// register it as an MCP server via `codex mcp add`. +/// - Use its `image_scenario` tool with cases like `text_then_image`, +/// `invalid_base64_then_image`, or `invalid_image_bytes_then_image` to ensure this path triggers +/// even when the first block is not a valid image. fn try_new_completed_mcp_tool_call_with_image_output( result: &Result, ) -> Option { - match result { - Ok(mcp_types::CallToolResult { content, .. }) => { - if let Some(mcp_types::ContentBlock::ImageContent(image)) = content.first() { - let raw_data = match base64::engine::general_purpose::STANDARD.decode(&image.data) { - Ok(data) => data, - Err(e) => { - error!("Failed to decode image data: {e}"); - return None; - } - }; - let reader = match ImageReader::new(Cursor::new(raw_data)).with_guessed_format() { - Ok(reader) => reader, - Err(e) => { - error!("Failed to guess image format: {e}"); - return None; - } - }; + let image = result + .as_ref() + .ok()? + .content + .iter() + .find_map(decode_mcp_image)?; - let image = match reader.decode() { - Ok(image) => image, - Err(e) => { - error!("Image decoding failed: {e}"); - return None; - } - }; + Some(CompletedMcpToolCallWithImageOutput { _image: image }) +} - Some(CompletedMcpToolCallWithImageOutput { _image: image }) - } else { - None - } - } - _ => None, - } +/// Decodes an MCP `ImageContent` block into an in-memory image. +/// +/// Returns `None` when the block is not an image, when base64 decoding fails, when the format +/// cannot be inferred, or when the image decoder rejects the bytes. +fn decode_mcp_image(block: &mcp_types::ContentBlock) -> Option { + let image = match block { + mcp_types::ContentBlock::ImageContent(image) => image, + _ => return None, + }; + let raw_data = base64::engine::general_purpose::STANDARD + .decode(&image.data) + .map_err(|e| { + error!("Failed to decode image data: {e}"); + e + }) + .ok()?; + let reader = ImageReader::new(Cursor::new(raw_data)) + .with_guessed_format() + .map_err(|e| { + error!("Failed to guess image format: {e}"); + e + }) + .ok()?; + + reader + .decode() + .map_err(|e| { + error!("Image decoding failed: {e}"); + e + }) + .ok() } #[allow(clippy::disallowed_methods)] @@ -1929,9 +1947,12 @@ mod tests { use codex_core::protocol::ExecCommandSource; use mcp_types::CallToolResult; use mcp_types::ContentBlock; + use mcp_types::ImageContent; use mcp_types::TextContent; use mcp_types::Tool; use mcp_types::ToolInputSchema; + + const SMALL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; async fn test_config() -> Config { let codex_home = std::env::temp_dir(); ConfigBuilder::default() @@ -1957,6 +1978,15 @@ mod tests { render_lines(&cell.transcript_lines(u16::MAX)) } + fn image_block(data: &str) -> ContentBlock { + ContentBlock::ImageContent(ImageContent { + annotations: None, + data: data.to_string(), + mime_type: "image/png".into(), + r#type: "image".into(), + }) + } + #[test] fn unified_exec_interaction_cell_renders_input() { let cell = @@ -2251,6 +2281,63 @@ mod tests { insta::assert_snapshot!(rendered); } + #[test] + fn completed_mcp_tool_call_image_after_text_returns_extra_cell() { + let invocation = McpInvocation { + server: "image".into(), + tool: "generate".into(), + arguments: Some(json!({ + "prompt": "tiny image", + })), + }; + + let result = CallToolResult { + content: vec![ + ContentBlock::TextContent(TextContent { + annotations: None, + text: "Here is the image:".into(), + r#type: "text".into(), + }), + image_block(SMALL_PNG_BASE64), + ], + is_error: None, + structured_content: None, + }; + + let mut cell = new_active_mcp_tool_call("call-image".into(), invocation, true); + let extra_cell = cell + .complete(Duration::from_millis(25), Ok(result)) + .expect("expected image cell"); + + let rendered = render_lines(&extra_cell.display_lines(80)); + assert_eq!(rendered, vec!["tool result (image output)"]); + } + + #[test] + fn completed_mcp_tool_call_skips_invalid_image_blocks() { + let invocation = McpInvocation { + server: "image".into(), + tool: "generate".into(), + arguments: Some(json!({ + "prompt": "tiny image", + })), + }; + + let result = CallToolResult { + content: vec![image_block("not-base64"), image_block(SMALL_PNG_BASE64)], + is_error: None, + structured_content: None, + }; + + let mut cell = new_active_mcp_tool_call("call-image-2".into(), invocation, true); + let extra_cell = cell + .complete(Duration::from_millis(25), Ok(result)) + .expect("expected image cell"); + + let rendered = render_lines(&extra_cell.display_lines(80)); + assert_eq!(rendered, vec!["tool result (image output)"]); + } + #[test] fn completed_mcp_tool_call_error_snapshot() { let invocation = McpInvocation { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0fac506d32..bced1e0653 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -26,13 +26,14 @@ use codex_core::config::resolve_oss_provider; use codex_core::config_loader::ConfigLoadError; use codex_core::config_loader::format_config_error_with_source; use codex_core::find_thread_path_by_id_str; -use codex_core::get_platform_sandbox; use codex_core::path_utils; use codex_core::protocol::AskForApproval; use codex_core::read_session_meta_line; use codex_core::terminal::Multiplexer; +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_utils_absolute_path::AbsolutePathBuf; @@ -816,7 +817,9 @@ async fn load_config_or_exit_with_fallback_cwd( /// or if the current cwd project is already trusted. If not, we need to /// show the trust screen. fn should_show_trust_screen(config: &Config) -> bool { - if cfg!(target_os = "windows") && get_platform_sandbox().is_none() { + if cfg!(target_os = "windows") + && WindowsSandboxLevel::from_config(config) == WindowsSandboxLevel::Disabled + { // If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely. return false; } @@ -879,7 +882,7 @@ mod tests { let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; - config.set_windows_sandbox_globally(false); + config.set_windows_sandbox_enabled(false); let should_show = should_show_trust_screen(&config); if cfg!(target_os = "windows") { @@ -902,7 +905,7 @@ mod tests { let mut config = build_config(&temp_dir).await?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; - config.set_windows_sandbox_globally(true); + config.set_windows_sandbox_enabled(true); let should_show = should_show_trust_screen(&config); if cfg!(target_os = "windows") { diff --git a/codex-rs/tui/src/notifications/bel.rs b/codex-rs/tui/src/notifications/bel.rs new file mode 100644 index 0000000000..44e7de2603 --- /dev/null +++ b/codex-rs/tui/src/notifications/bel.rs @@ -0,0 +1,37 @@ +use std::fmt; +use std::io; +use std::io::stdout; + +use crossterm::Command; +use ratatui::crossterm::execute; + +#[derive(Debug, Default)] +pub struct BelBackend; + +impl BelBackend { + pub fn notify(&mut self, _message: &str) -> io::Result<()> { + execute!(stdout(), PostNotification) + } +} + +/// Command that emits a BEL desktop notification. +#[derive(Debug, Clone)] +pub struct PostNotification; + +impl Command for PostNotification { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + write!(f, "\x07") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + Err(std::io::Error::other( + "tried to execute PostNotification using WinAPI; use ANSI instead", + )) + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } +} diff --git a/codex-rs/tui/src/notifications/mod.rs b/codex-rs/tui/src/notifications/mod.rs index 33a591dfdb..e9a7ac1ddf 100644 --- a/codex-rs/tui/src/notifications/mod.rs +++ b/codex-rs/tui/src/notifications/mod.rs @@ -1,68 +1,80 @@ +mod bel; mod osc9; -mod windows_toast; use std::env; use std::io; -use codex_core::env::is_wsl; +use bel::BelBackend; +use codex_core::config::types::NotificationMethod; use osc9::Osc9Backend; -use windows_toast::WindowsToastBackend; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NotificationBackendKind { - Osc9, - WindowsToast, -} #[derive(Debug)] pub enum DesktopNotificationBackend { Osc9(Osc9Backend), - WindowsToast(WindowsToastBackend), + Bel(BelBackend), } impl DesktopNotificationBackend { - pub fn osc9() -> Self { - Self::Osc9(Osc9Backend) + pub fn for_method(method: NotificationMethod) -> Self { + match method { + NotificationMethod::Auto => { + if supports_osc9() { + Self::Osc9(Osc9Backend) + } else { + Self::Bel(BelBackend) + } + } + NotificationMethod::Osc9 => Self::Osc9(Osc9Backend), + NotificationMethod::Bel => Self::Bel(BelBackend), + } } - pub fn windows_toast() -> Self { - Self::WindowsToast(WindowsToastBackend::default()) - } - - pub fn kind(&self) -> NotificationBackendKind { + pub fn method(&self) -> NotificationMethod { match self { - DesktopNotificationBackend::Osc9(_) => NotificationBackendKind::Osc9, - DesktopNotificationBackend::WindowsToast(_) => NotificationBackendKind::WindowsToast, + DesktopNotificationBackend::Osc9(_) => NotificationMethod::Osc9, + DesktopNotificationBackend::Bel(_) => NotificationMethod::Bel, } } pub fn notify(&mut self, message: &str) -> io::Result<()> { match self { DesktopNotificationBackend::Osc9(backend) => backend.notify(message), - DesktopNotificationBackend::WindowsToast(backend) => backend.notify(message), + DesktopNotificationBackend::Bel(backend) => backend.notify(message), } } } -pub fn detect_backend() -> DesktopNotificationBackend { - if should_use_windows_toasts() { - tracing::info!( - "Windows Terminal session detected under WSL; using Windows toast notifications" - ); - DesktopNotificationBackend::windows_toast() - } else { - DesktopNotificationBackend::osc9() - } +pub fn detect_backend(method: NotificationMethod) -> DesktopNotificationBackend { + DesktopNotificationBackend::for_method(method) } -fn should_use_windows_toasts() -> bool { - is_wsl() && env::var_os("WT_SESSION").is_some() +fn supports_osc9() -> bool { + if env::var_os("WT_SESSION").is_some() { + return false; + } + // Prefer TERM_PROGRAM when present, but keep fallbacks for shells/launchers + // that don't set it (e.g., tmux/ssh) to avoid regressing OSC 9 support. + if matches!( + env::var("TERM_PROGRAM").ok().as_deref(), + Some("WezTerm" | "ghostty") + ) { + return true; + } + // iTerm still provides a strong session signal even when TERM_PROGRAM is missing. + if env::var_os("ITERM_SESSION_ID").is_some() { + return true; + } + // TERM-based hints cover kitty/wezterm setups without TERM_PROGRAM. + matches!( + env::var("TERM").ok().as_deref(), + Some("xterm-kitty" | "wezterm" | "wezterm-mux") + ) } #[cfg(test)] mod tests { - use super::NotificationBackendKind; use super::detect_backend; + use codex_core::config::types::NotificationMethod; use serial_test::serial; use std::ffi::OsString; @@ -101,39 +113,44 @@ mod tests { } #[test] - #[serial] - fn defaults_to_osc9_outside_wsl() { - let _wsl_guard = EnvVarGuard::remove("WSL_DISTRO_NAME"); - let _wt_guard = EnvVarGuard::remove("WT_SESSION"); - assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9); + fn selects_osc9_method() { + assert!(matches!( + detect_backend(NotificationMethod::Osc9), + super::DesktopNotificationBackend::Osc9(_) + )); + } + + #[test] + fn selects_bel_method() { + assert!(matches!( + detect_backend(NotificationMethod::Bel), + super::DesktopNotificationBackend::Bel(_) + )); } #[test] #[serial] - fn waits_for_windows_terminal() { - let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu"); - let _wt_guard = EnvVarGuard::remove("WT_SESSION"); - assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9); + fn auto_prefers_bel_without_hints() { + let _term = EnvVarGuard::remove("TERM"); + let _term_program = EnvVarGuard::remove("TERM_PROGRAM"); + let _iterm = EnvVarGuard::remove("ITERM_SESSION_ID"); + let _wt = EnvVarGuard::remove("WT_SESSION"); + assert!(matches!( + detect_backend(NotificationMethod::Auto), + super::DesktopNotificationBackend::Bel(_) + )); } - #[cfg(target_os = "linux")] #[test] #[serial] - fn selects_windows_toast_in_wsl_windows_terminal() { - let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu"); - let _wt_guard = EnvVarGuard::set("WT_SESSION", "abc"); - assert_eq!( - detect_backend().kind(), - NotificationBackendKind::WindowsToast - ); - } - - #[cfg(not(target_os = "linux"))] - #[test] - #[serial] - fn stays_on_osc9_outside_linux_even_with_wsl_env() { - let _wsl_guard = EnvVarGuard::set("WSL_DISTRO_NAME", "Ubuntu"); - let _wt_guard = EnvVarGuard::set("WT_SESSION", "abc"); - assert_eq!(detect_backend().kind(), NotificationBackendKind::Osc9); + fn auto_uses_osc9_for_iterm() { + let _term = EnvVarGuard::remove("TERM"); + let _term_program = EnvVarGuard::remove("TERM_PROGRAM"); + let _iterm = EnvVarGuard::set("ITERM_SESSION_ID", "abc"); + let _wt = EnvVarGuard::remove("WT_SESSION"); + assert!(matches!( + detect_backend(NotificationMethod::Auto), + super::DesktopNotificationBackend::Osc9(_) + )); } } diff --git a/codex-rs/tui/src/notifications/windows_toast.rs b/codex-rs/tui/src/notifications/windows_toast.rs deleted file mode 100644 index 9917e62c37..0000000000 --- a/codex-rs/tui/src/notifications/windows_toast.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::io; -use std::process::Command; -use std::process::Stdio; - -use base64::Engine as _; -use base64::engine::general_purpose::STANDARD as BASE64; - -const APP_ID: &str = "Codex"; -const POWERSHELL_EXE: &str = "powershell.exe"; - -#[derive(Debug)] -pub struct WindowsToastBackend { - encoded_title: String, -} - -impl WindowsToastBackend { - pub fn notify(&mut self, message: &str) -> io::Result<()> { - let encoded_body = encode_argument(message); - let encoded_command = build_encoded_command(&self.encoded_title, &encoded_body); - spawn_powershell(encoded_command) - } -} - -impl Default for WindowsToastBackend { - fn default() -> Self { - WindowsToastBackend { - encoded_title: encode_argument(APP_ID), - } - } -} - -fn spawn_powershell(encoded_command: String) -> io::Result<()> { - let mut command = Command::new(POWERSHELL_EXE); - command - .arg("-NoProfile") - .arg("-NoLogo") - .arg("-EncodedCommand") - .arg(encoded_command) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - let status = command.status()?; - if status.success() { - Ok(()) - } else { - Err(io::Error::other(format!( - "{POWERSHELL_EXE} exited with status {status}" - ))) - } -} - -fn build_encoded_command(encoded_title: &str, encoded_body: &str) -> String { - let script = build_ps_script(encoded_title, encoded_body); - encode_script_for_powershell(&script) -} - -fn build_ps_script(encoded_title: &str, encoded_body: &str) -> String { - format!( - r#" -$encoding = [System.Text.Encoding]::UTF8 -$titleText = $encoding.GetString([System.Convert]::FromBase64String("{encoded_title}")) -$bodyText = $encoding.GetString([System.Convert]::FromBase64String("{encoded_body}")) -[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null -$doc = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02) -$textNodes = $doc.GetElementsByTagName("text") -$textNodes.Item(0).AppendChild($doc.CreateTextNode($titleText)) | Out-Null -$textNodes.Item(1).AppendChild($doc.CreateTextNode($bodyText)) | Out-Null -$toast = [Windows.UI.Notifications.ToastNotification]::new($doc) -[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Codex').Show($toast) -"#, - ) -} - -fn encode_script_for_powershell(script: &str) -> String { - let mut wide: Vec = Vec::with_capacity((script.len() + 1) * 2); - for unit in script.encode_utf16() { - let bytes = unit.to_le_bytes(); - wide.extend_from_slice(&bytes); - } - BASE64.encode(wide) -} - -fn encode_argument(value: &str) -> String { - BASE64.encode(escape_for_xml(value)) -} - -pub fn escape_for_xml(input: &str) -> String { - let mut escaped = String::with_capacity(input.len()); - for ch in input.chars() { - match ch { - '&' => escaped.push_str("&"), - '<' => escaped.push_str("<"), - '>' => escaped.push_str(">"), - '"' => escaped.push_str("""), - '\'' => escaped.push_str("'"), - _ => escaped.push(ch), - } - } - escaped -} - -#[cfg(test)] -mod tests { - use super::encode_script_for_powershell; - use super::escape_for_xml; - use pretty_assertions::assert_eq; - - #[test] - fn escapes_xml_entities() { - assert_eq!(escape_for_xml("5 > 3"), "5 > 3"); - assert_eq!(escape_for_xml("a & b"), "a & b"); - assert_eq!(escape_for_xml(""), "<tag>"); - assert_eq!(escape_for_xml("\"quoted\""), ""quoted""); - assert_eq!(escape_for_xml("single 'quote'"), "single 'quote'"); - } - - #[test] - fn leaves_safe_text_unmodified() { - assert_eq!(escape_for_xml("codex"), "codex"); - assert_eq!(escape_for_xml("multi word text"), "multi word text"); - } - - #[test] - fn encodes_utf16le_for_powershell() { - assert_eq!(encode_script_for_powershell("A"), "QQA="); - } -} diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index b5fee2e425..761fa83627 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -39,12 +39,12 @@ pub use self::frame_requester::FrameRequester; use crate::custom_terminal; use crate::custom_terminal::Terminal as CustomTerminal; use crate::notifications::DesktopNotificationBackend; -use crate::notifications::NotificationBackendKind; use crate::notifications::detect_backend; use crate::tui::event_stream::EventBroker; use crate::tui::event_stream::TuiEventStream; #[cfg(unix)] use crate::tui::job_control::SuspendContext; +use codex_core::config::types::NotificationMethod; mod event_stream; mod frame_rate_limiter; @@ -275,7 +275,7 @@ impl Tui { alt_screen_active: Arc::new(AtomicBool::new(false)), terminal_focused: Arc::new(AtomicBool::new(true)), enhanced_keys_supported, - notification_backend: Some(detect_backend()), + notification_backend: Some(detect_backend(NotificationMethod::default())), alt_screen_enabled: true, } } @@ -285,6 +285,10 @@ impl Tui { self.alt_screen_enabled = enabled; } + pub fn set_notification_method(&mut self, method: NotificationMethod) { + self.notification_backend = Some(detect_backend(method)); + } + pub fn frame_requester(&self) -> FrameRequester { self.frame_requester.clone() } @@ -361,36 +365,16 @@ impl Tui { let message = message.as_ref().to_string(); match backend.notify(&message) { Ok(()) => true, - Err(err) => match backend.kind() { - NotificationBackendKind::WindowsToast => { - tracing::error!( - error = %err, - "Failed to send Windows toast notification; falling back to OSC 9" - ); - self.notification_backend = Some(DesktopNotificationBackend::osc9()); - if let Some(backend) = self.notification_backend.as_mut() { - if let Err(osc_err) = backend.notify(&message) { - tracing::warn!( - error = %osc_err, - "Failed to emit OSC 9 notification after toast fallback; \ - disabling future notifications" - ); - self.notification_backend = None; - return false; - } - return true; - } - false - } - NotificationBackendKind::Osc9 => { - tracing::warn!( - error = %err, - "Failed to emit OSC 9 notification; disabling future notifications" - ); - self.notification_backend = None; - false - } - }, + Err(err) => { + let method = backend.method(); + tracing::warn!( + error = %err, + method = %method, + "Failed to emit terminal notification; disabling future notifications" + ); + self.notification_backend = None; + false + } } } diff --git a/codex-rs/windows-sandbox-rs/src/firewall.rs b/codex-rs/windows-sandbox-rs/src/firewall.rs index c75214bb36..b4939fd42b 100644 --- a/codex-rs/windows-sandbox-rs/src/firewall.rs +++ b/codex-rs/windows-sandbox-rs/src/firewall.rs @@ -21,6 +21,9 @@ use windows::Win32::System::Com::CoUninitialize; use windows::Win32::System::Com::CLSCTX_INPROC_SERVER; use windows::Win32::System::Com::COINIT_APARTMENTTHREADED; +use codex_windows_sandbox::SetupErrorCode; +use codex_windows_sandbox::SetupFailure; + // This is the stable identifier we use to find/update the rule idempotently. // It intentionally does not change between installs. const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound"; @@ -33,16 +36,27 @@ pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Resul let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; if hr.is_err() { - return Err(anyhow::anyhow!("CoInitializeEx failed: {hr:?}")); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallComInitFailed, + format!("CoInitializeEx failed: {hr:?}"), + ))); } let result = unsafe { (|| -> Result<()> { let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER) - .map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwPolicy2: {e:?}"))?; - let rules = policy - .Rules() - .map_err(|e| anyhow::anyhow!("INetFwPolicy2::Rules: {e:?}"))?; + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallPolicyAccessFailed, + format!("CoCreateInstance NetFwPolicy2 failed: {err:?}"), + )) + })?; + let rules = policy.Rules().map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallPolicyAccessFailed, + format!("INetFwPolicy2::Rules failed: {err:?}"), + )) + })?; // Block all outbound IP protocols for this user. ensure_block_rule( @@ -75,14 +89,28 @@ fn ensure_block_rule( ) -> Result<()> { let name = BSTR::from(internal_name); let rule: INetFwRule3 = match unsafe { rules.Item(&name) } { - Ok(existing) => existing - .cast() - .map_err(|e| anyhow::anyhow!("cast existing firewall rule to INetFwRule3: {e:?}"))?, + Ok(existing) => existing.cast().map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("cast existing firewall rule to INetFwRule3 failed: {err:?}"), + )) + })?, Err(_) => { let new_rule: INetFwRule3 = - unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) } - .map_err(|e| anyhow::anyhow!("CoCreateInstance NetFwRule: {e:?}"))?; - unsafe { new_rule.SetName(&name) }.map_err(|e| anyhow::anyhow!("SetName: {e:?}"))?; + unsafe { CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER) }.map_err( + |err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("CoCreateInstance NetFwRule failed: {err:?}"), + )) + }, + )?; + unsafe { new_rule.SetName(&name) }.map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetName failed: {err:?}"), + )) + })?; // Set all properties before adding the rule so we don't leave half-configured rules. configure_rule( &new_rule, @@ -91,7 +119,12 @@ fn ensure_block_rule( local_user_spec, offline_sid, )?; - unsafe { rules.Add(&new_rule) }.map_err(|e| anyhow::anyhow!("Rules::Add: {e:?}"))?; + unsafe { rules.Add(&new_rule) }.map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("Rules::Add failed: {err:?}"), + )) + })?; new_rule } }; @@ -117,29 +150,66 @@ fn configure_rule( ) -> Result<()> { unsafe { rule.SetDescription(&BSTR::from(friendly_desc)) - .map_err(|e| anyhow::anyhow!("SetDescription: {e:?}"))?; - rule.SetDirection(NET_FW_RULE_DIR_OUT) - .map_err(|e| anyhow::anyhow!("SetDirection: {e:?}"))?; - rule.SetAction(NET_FW_ACTION_BLOCK) - .map_err(|e| anyhow::anyhow!("SetAction: {e:?}"))?; - rule.SetEnabled(VARIANT_TRUE) - .map_err(|e| anyhow::anyhow!("SetEnabled: {e:?}"))?; - rule.SetProfiles(NET_FW_PROFILE2_ALL.0) - .map_err(|e| anyhow::anyhow!("SetProfiles: {e:?}"))?; - rule.SetProtocol(protocol) - .map_err(|e| anyhow::anyhow!("SetProtocol: {e:?}"))?; + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetDescription failed: {err:?}"), + )) + })?; + rule.SetDirection(NET_FW_RULE_DIR_OUT).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetDirection failed: {err:?}"), + )) + })?; + rule.SetAction(NET_FW_ACTION_BLOCK).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetAction failed: {err:?}"), + )) + })?; + rule.SetEnabled(VARIANT_TRUE).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetEnabled failed: {err:?}"), + )) + })?; + rule.SetProfiles(NET_FW_PROFILE2_ALL.0).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetProfiles failed: {err:?}"), + )) + })?; + rule.SetProtocol(protocol).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetProtocol failed: {err:?}"), + )) + })?; rule.SetLocalUserAuthorizedList(&BSTR::from(local_user_spec)) - .map_err(|e| anyhow::anyhow!("SetLocalUserAuthorizedList: {e:?}"))?; + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("SetLocalUserAuthorizedList failed: {err:?}"), + )) + })?; } // Read-back verification: ensure we actually wrote the expected SID scope. - let actual = unsafe { rule.LocalUserAuthorizedList() } - .map_err(|e| anyhow::anyhow!("LocalUserAuthorizedList (read-back): {e:?}"))?; + let actual = unsafe { rule.LocalUserAuthorizedList() }.map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleVerifyFailed, + format!("LocalUserAuthorizedList (read-back) failed: {err:?}"), + )) + })?; let actual_str = actual.to_string(); if !actual_str.contains(offline_sid) { - anyhow::bail!( - "offline firewall rule user scope mismatch: expected SID {offline_sid}, got {actual_str}" - ); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleVerifyFailed, + format!( + "offline firewall rule user scope mismatch: expected SID {offline_sid}, got {actual_str}" + ), + ))); } Ok(()) } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 62fda67655..fc062d6fd5 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -16,6 +16,9 @@ mod setup; #[cfg(target_os = "windows")] mod elevated_impl; +#[cfg(target_os = "windows")] +mod setup_error; + #[cfg(target_os = "windows")] pub use acl::allow_null_device; #[cfg(target_os = "windows")] @@ -67,6 +70,20 @@ pub use setup::sandbox_secrets_dir; #[cfg(target_os = "windows")] pub use setup::SETUP_VERSION; #[cfg(target_os = "windows")] +pub use setup_error::extract_failure as extract_setup_failure; +#[cfg(target_os = "windows")] +pub use setup_error::sanitize_tag_value as sanitize_setup_metric_tag_value; +#[cfg(target_os = "windows")] +pub use setup_error::setup_error_path; +#[cfg(target_os = "windows")] +pub use setup_error::write_setup_error_report; +#[cfg(target_os = "windows")] +pub use setup_error::SetupErrorCode; +#[cfg(target_os = "windows")] +pub use setup_error::SetupErrorReport; +#[cfg(target_os = "windows")] +pub use setup_error::SetupFailure; +#[cfg(target_os = "windows")] pub use token::convert_string_sid_to_sid; #[cfg(target_os = "windows")] pub use token::create_readonly_token_with_cap_from; diff --git a/codex-rs/windows-sandbox-rs/src/sandbox_users.rs b/codex-rs/windows-sandbox-rs/src/sandbox_users.rs index 5847363efb..5ab39c3cec 100644 --- a/codex-rs/windows-sandbox-rs/src/sandbox_users.rs +++ b/codex-rs/windows-sandbox-rs/src/sandbox_users.rs @@ -39,6 +39,8 @@ use codex_windows_sandbox::sandbox_dir; use codex_windows_sandbox::sandbox_secrets_dir; use codex_windows_sandbox::string_from_sid_bytes; use codex_windows_sandbox::to_wide; +use codex_windows_sandbox::SetupErrorCode; +use codex_windows_sandbox::SetupFailure; use codex_windows_sandbox::SETUP_VERSION; pub const SANDBOX_USERS_GROUP: &str = "CodexSandboxUsers"; @@ -122,9 +124,10 @@ pub fn ensure_local_user(name: &str, password: &str, log: &mut File) -> Result<( ); if upd != NERR_Success { super::log_line(log, &format!("NetUserSetInfo failed for {name} code {upd}"))?; - return Err(anyhow::anyhow!( - "failed to create/update user {name}, code {status}/{upd}" - )); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUserCreateOrUpdateFailed, + format!("failed to create/update user {name}, code {status}/{upd}"), + ))); } } @@ -174,7 +177,10 @@ pub fn ensure_local_group(name: &str, comment: &str, log: &mut File) -> Result<( log, &format!("NetLocalGroupAdd failed for {name} code {status} parm_err={parm_err}"), )?; - anyhow::bail!("failed to create local group {name}, code {status}"); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersGroupCreateFailed, + format!("failed to create local group {name}, code {status}"), + ))); } } Ok(()) @@ -394,11 +400,37 @@ fn write_secrets( online_pwd: &str, ) -> Result<()> { let sandbox_dir = sandbox_dir(codex_home); - std::fs::create_dir_all(&sandbox_dir)?; + std::fs::create_dir_all(&sandbox_dir).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersFileWriteFailed, + format!( + "failed to create sandbox dir {}: {err}", + sandbox_dir.display() + ), + )) + })?; let secrets_dir = sandbox_secrets_dir(codex_home); - std::fs::create_dir_all(&secrets_dir)?; - let offline_blob = dpapi_protect(offline_pwd.as_bytes())?; - let online_blob = dpapi_protect(online_pwd.as_bytes())?; + std::fs::create_dir_all(&secrets_dir).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersFileWriteFailed, + format!( + "failed to create secrets dir {}: {err}", + secrets_dir.display() + ), + )) + })?; + let offline_blob = dpapi_protect(offline_pwd.as_bytes()).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperDpapiProtectFailed, + format!("dpapi protect failed for offline user: {err}"), + )) + })?; + let online_blob = dpapi_protect(online_pwd.as_bytes()).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperDpapiProtectFailed, + format!("dpapi protect failed for online user: {err}"), + )) + })?; let users = SandboxUsersFile { version: SETUP_VERSION, offline: SandboxUserRecord { @@ -420,7 +452,35 @@ fn write_secrets( }; let users_path = secrets_dir.join("sandbox_users.json"); let marker_path = sandbox_dir.join("setup_marker.json"); - std::fs::write(users_path, serde_json::to_vec_pretty(&users)?)?; - std::fs::write(marker_path, serde_json::to_vec_pretty(&marker)?)?; + let users_json = serde_json::to_vec_pretty(&users).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersFileWriteFailed, + format!("serialize sandbox users failed: {err}"), + )) + })?; + std::fs::write(&users_path, users_json).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUsersFileWriteFailed, + format!( + "write sandbox users file {} failed: {err}", + users_path.display() + ), + )) + })?; + let marker_json = serde_json::to_vec_pretty(&marker).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSetupMarkerWriteFailed, + format!("serialize setup marker failed: {err}"), + )) + })?; + std::fs::write(&marker_path, marker_json).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSetupMarkerWriteFailed, + format!( + "write setup marker file {} failed: {err}", + marker_path.display() + ), + )) + })?; Ok(()) } diff --git a/codex-rs/windows-sandbox-rs/src/setup_error.rs b/codex-rs/windows-sandbox-rs/src/setup_error.rs new file mode 100644 index 0000000000..21d260b4d4 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/setup_error.rs @@ -0,0 +1,296 @@ +use anyhow::Context; +use anyhow::Result; +use serde::Deserialize; +use serde::Serialize; +use std::fs; +use std::io::ErrorKind; +use std::path::Path; +use std::path::PathBuf; + +/// These represent the most common failures for the elevated sandbox setup. +/// +/// Codes are used as metric tags. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SetupErrorCode { + // Orchestrator (run in CLI) failures. + /// Failed to create `codex_home/.sandbox` in the orchestrator. + OrchestratorSandboxDirCreateFailed, + /// Failed to determine whether the current process is elevated. + OrchestratorElevationCheckFailed, + /// Failed to serialize the elevation payload before launching the helper. + OrchestratorPayloadSerializeFailed, + /// Failed to launch the setup helper process (spawn or ShellExecuteExW). + OrchestratorHelperLaunchFailed, + /// Helper exited non-zero and no structured report was available. + OrchestratorHelperExitNonzero, + /// Helper exited non-zero and reading `setup_error.json` failed. + OrchestratorHelperReportReadFailed, + // Helper (elevated process) failures. + /// Helper failed while validating or decoding the request payload. + HelperRequestArgsFailed, + /// Helper failed to create `codex_home/.sandbox`. + HelperSandboxDirCreateFailed, + /// Helper failed to open or write the setup log. + HelperLogFailed, + /// Helper failed in the provisioning phase (fallback bucket). + HelperUserProvisionFailed, + /// Helper failed to create the sandbox users local group. + HelperUsersGroupCreateFailed, + /// Helper failed to create or update a sandbox user account. + HelperUserCreateOrUpdateFailed, + /// Helper failed to protect user passwords with DPAPI. + HelperDpapiProtectFailed, + /// Helper failed to write the sandbox users secrets file. + HelperUsersFileWriteFailed, + /// Helper failed to write the setup marker file. + HelperSetupMarkerWriteFailed, + /// Helper failed to resolve a SID or convert it to a PSID. + HelperSidResolveFailed, + /// Helper failed to load or convert capability SIDs. + HelperCapabilitySidFailed, + /// Helper failed to initialize COM for firewall configuration. + HelperFirewallComInitFailed, + /// Helper failed to access firewall policy or rule collections. + HelperFirewallPolicyAccessFailed, + /// Helper failed to create, update, or add the firewall rule. + HelperFirewallRuleCreateOrAddFailed, + /// Helper failed to verify the configured firewall rule scope. + HelperFirewallRuleVerifyFailed, + /// Helper failed to spawn the read-ACL helper process. + HelperReadAclHelperSpawnFailed, + /// Helper failed to lock down sandbox directories via ACLs. + HelperSandboxLockFailed, + /// Helper failed for an unmapped or unexpected reason. + HelperUnknownError, +} + +impl SetupErrorCode { + pub fn as_str(self) -> &'static str { + match self { + Self::OrchestratorSandboxDirCreateFailed => "orchestrator_sandbox_dir_create_failed", + Self::OrchestratorElevationCheckFailed => "orchestrator_elevation_check_failed", + Self::OrchestratorPayloadSerializeFailed => "orchestrator_payload_serialize_failed", + Self::OrchestratorHelperLaunchFailed => "orchestrator_helper_launch_failed", + Self::OrchestratorHelperExitNonzero => "orchestrator_helper_exit_nonzero", + Self::OrchestratorHelperReportReadFailed => "orchestrator_helper_report_read_failed", + Self::HelperRequestArgsFailed => "helper_request_args_failed", + Self::HelperSandboxDirCreateFailed => "helper_sandbox_dir_create_failed", + Self::HelperLogFailed => "helper_log_failed", + Self::HelperUserProvisionFailed => "helper_user_provision_failed", + Self::HelperUsersGroupCreateFailed => "helper_users_group_create_failed", + Self::HelperUserCreateOrUpdateFailed => "helper_user_create_or_update_failed", + Self::HelperDpapiProtectFailed => "helper_dpapi_protect_failed", + Self::HelperUsersFileWriteFailed => "helper_users_file_write_failed", + Self::HelperSetupMarkerWriteFailed => "helper_setup_marker_write_failed", + Self::HelperSidResolveFailed => "helper_sid_resolve_failed", + Self::HelperCapabilitySidFailed => "helper_capability_sid_failed", + Self::HelperFirewallComInitFailed => "helper_firewall_com_init_failed", + Self::HelperFirewallPolicyAccessFailed => "helper_firewall_policy_access_failed", + Self::HelperFirewallRuleCreateOrAddFailed => { + "helper_firewall_rule_create_or_add_failed" + } + Self::HelperFirewallRuleVerifyFailed => "helper_firewall_rule_verify_failed", + Self::HelperReadAclHelperSpawnFailed => "helper_read_acl_helper_spawn_failed", + Self::HelperSandboxLockFailed => "helper_sandbox_lock_failed", + Self::HelperUnknownError => "helper_unknown_error", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetupErrorReport { + pub code: SetupErrorCode, + pub message: String, +} + +#[derive(Debug)] +pub struct SetupFailure { + pub code: SetupErrorCode, + pub message: String, +} + +impl SetupFailure { + pub fn new(code: SetupErrorCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + pub fn from_report(report: SetupErrorReport) -> Self { + Self::new(report.code, report.message) + } + + pub fn metric_message(&self) -> String { + sanitize_tag_value(&self.message) + } +} + +impl std::fmt::Display for SetupFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code.as_str(), self.message) + } +} + +impl std::error::Error for SetupFailure {} + +pub fn failure(code: SetupErrorCode, message: impl Into) -> anyhow::Error { + anyhow::Error::new(SetupFailure::new(code, message)) +} + +pub fn extract_failure(err: &anyhow::Error) -> Option<&SetupFailure> { + err.downcast_ref::() +} + +pub fn setup_error_path(codex_home: &Path) -> PathBuf { + codex_home.join(".sandbox").join("setup_error.json") +} + +pub fn clear_setup_error_report(codex_home: &Path) -> Result<()> { + let path = setup_error_path(codex_home); + match fs::remove_file(&path) { + Ok(()) => Ok(()), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), + Err(err) => Err(err).with_context(|| format!("remove {}", path.display())), + } +} + +pub fn write_setup_error_report(codex_home: &Path, report: &SetupErrorReport) -> Result<()> { + let sandbox_dir = codex_home.join(".sandbox"); + fs::create_dir_all(&sandbox_dir) + .with_context(|| format!("create sandbox dir {}", sandbox_dir.display()))?; + let path = setup_error_path(codex_home); + let json = serde_json::to_vec_pretty(report)?; + fs::write(&path, json).with_context(|| format!("write {}", path.display()))?; + Ok(()) +} + +pub fn read_setup_error_report(codex_home: &Path) -> Result> { + let path = setup_error_path(codex_home); + let bytes = match fs::read(&path) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err).with_context(|| format!("read {}", path.display())), + }; + let report = serde_json::from_slice::(&bytes) + .with_context(|| format!("parse {}", path.display()))?; + Ok(Some(report)) +} + +/// Sanitize a tag value to comply with metric tag validation rules: +/// only ASCII alphanumeric, '.', '_', '-', and '/' are allowed. +pub fn sanitize_tag_value(value: &str) -> String { + const MAX_LEN: usize = 256; + let redacted = redact_home_paths(value); + let sanitized: String = redacted + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/') { + ch + } else { + '_' + } + }) + .collect(); + let trimmed = sanitized.trim_matches('_'); + if trimmed.is_empty() { + return "unspecified".to_string(); + } + if trimmed.len() <= MAX_LEN { + trimmed.to_string() + } else { + trimmed[..MAX_LEN].to_string() + } +} + +fn redact_home_paths(value: &str) -> String { + let mut usernames: Vec = Vec::new(); + if let Ok(username) = std::env::var("USERNAME") { + if !username.trim().is_empty() { + usernames.push(username); + } + } + if let Ok(user) = std::env::var("USER") { + if !user.trim().is_empty() && !usernames.iter().any(|v| v.eq_ignore_ascii_case(&user)) { + usernames.push(user); + } + } + + redact_username_segments(value, &usernames) +} + +fn redact_username_segments(value: &str, usernames: &[String]) -> String { + if usernames.is_empty() { + return value.to_string(); + } + + let mut segments: Vec = Vec::new(); + let mut separators: Vec = Vec::new(); + let mut current = String::new(); + + for ch in value.chars() { + if ch == '\\' || ch == '/' { + segments.push(std::mem::take(&mut current)); + separators.push(ch); + } else { + current.push(ch); + } + } + segments.push(current); + + for segment in &mut segments { + let matches = if cfg!(windows) { + usernames + .iter() + .any(|name| segment.eq_ignore_ascii_case(name)) + } else { + usernames.iter().any(|name| segment == name) + }; + if matches { + *segment = "".to_string(); + } + } + + let mut out = String::new(); + for (idx, segment) in segments.iter().enumerate() { + out.push_str(segment); + if let Some(sep) = separators.get(idx) { + out.push(*sep); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn sanitize_tag_value_redacts_username_segments() { + let usernames = vec!["Alice".to_string(), "Bob".to_string()]; + let msg = "failed to write C:\\Users\\Alice\\file.txt; fallback D:\\Profiles\\Bob\\x"; + let redacted = redact_username_segments(msg, &usernames); + assert_eq!( + redacted, + "failed to write C:\\Users\\\\file.txt; fallback D:\\Profiles\\\\x" + ); + } + + #[test] + fn sanitize_tag_value_leaves_unknown_segments() { + let usernames = vec!["Alice".to_string()]; + let msg = "failed to write E:\\data\\file.txt"; + let redacted = redact_username_segments(msg, &usernames); + assert_eq!(redacted, msg); + } + + #[test] + fn sanitize_tag_value_redacts_multiple_occurrences() { + let usernames = vec!["Alice".to_string()]; + let msg = "C:\\Users\\Alice\\a and C:\\Users\\Alice\\b"; + let redacted = redact_username_segments(msg, &usernames); + assert_eq!(redacted, "C:\\Users\\\\a and C:\\Users\\\\b"); + } +} diff --git a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs index 39c8a95d4c..6868028782 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_main_win.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_main_win.rs @@ -9,6 +9,7 @@ use base64::Engine; use codex_windows_sandbox::convert_string_sid_to_sid; use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance; use codex_windows_sandbox::ensure_allow_write_aces; +use codex_windows_sandbox::extract_setup_failure; use codex_windows_sandbox::hide_newly_created_users; use codex_windows_sandbox::load_or_create_cap_sids; use codex_windows_sandbox::log_note; @@ -17,6 +18,10 @@ use codex_windows_sandbox::sandbox_dir; use codex_windows_sandbox::sandbox_secrets_dir; use codex_windows_sandbox::string_from_sid_bytes; use codex_windows_sandbox::to_wide; +use codex_windows_sandbox::write_setup_error_report; +use codex_windows_sandbox::SetupErrorCode; +use codex_windows_sandbox::SetupErrorReport; +use codex_windows_sandbox::SetupFailure; use codex_windows_sandbox::LOG_FILE_NAME; use codex_windows_sandbox::SETUP_VERSION; use serde::Deserialize; @@ -89,7 +94,12 @@ enum SetupMode { fn log_line(log: &mut File, msg: &str) -> Result<()> { let ts = chrono::Utc::now().to_rfc3339(); - writeln!(log, "[{ts}] {msg}")?; + writeln!(log, "[{ts}] {msg}").map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperLogFailed, + format!("failed to write setup log line: {err}"), + )) + })?; Ok(()) } @@ -349,29 +359,74 @@ pub fn main() -> Result<()> { fn real_main() -> Result<()> { let mut args = std::env::args().collect::>(); if args.len() != 2 { - anyhow::bail!("expected payload argument"); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperRequestArgsFailed, + "expected payload argument", + ))); } let payload_b64 = args.remove(1); - let payload_json = BASE64 - .decode(payload_b64) - .context("failed to decode payload b64")?; - let payload: Payload = - serde_json::from_slice(&payload_json).context("failed to parse payload json")?; + let payload_json = BASE64.decode(payload_b64).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperRequestArgsFailed, + format!("failed to decode payload b64: {err}"), + )) + })?; + let payload: Payload = serde_json::from_slice(&payload_json).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperRequestArgsFailed, + format!("failed to parse payload json: {err}"), + )) + })?; if payload.version != SETUP_VERSION { - anyhow::bail!("setup version mismatch"); + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperRequestArgsFailed, + format!( + "setup version mismatch: expected {SETUP_VERSION}, got {}", + payload.version + ), + ))); } let sbx_dir = sandbox_dir(&payload.codex_home); - std::fs::create_dir_all(&sbx_dir)?; + std::fs::create_dir_all(&sbx_dir).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxDirCreateFailed, + format!("failed to create sandbox dir {}: {err}", sbx_dir.display()), + )) + })?; let log_path = sbx_dir.join(LOG_FILE_NAME); let mut log = File::options() .create(true) .append(true) .open(&log_path) - .context("open log")?; + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperLogFailed, + format!("open log {} failed: {err}", log_path.display()), + )) + })?; let result = run_setup(&payload, &mut log, &sbx_dir); if let Err(err) = &result { let _ = log_line(&mut log, &format!("setup error: {err:?}")); log_note(&format!("setup error: {err:?}"), Some(sbx_dir.as_path())); + let failure = extract_setup_failure(err) + .map(|f| SetupFailure::new(f.code, f.message.clone())) + .unwrap_or_else(|| { + SetupFailure::new(SetupErrorCode::HelperUnknownError, err.to_string()) + }); + let report = SetupErrorReport { + code: failure.code, + message: failure.message.clone(), + }; + if let Err(write_err) = write_setup_error_report(&payload.codex_home, &report) { + let _ = log_line( + &mut log, + &format!("setup error report write failed: {write_err}"), + ); + log_note( + &format!("setup error report write failed: {write_err}"), + Some(sbx_dir.as_path()), + ); + } } result } @@ -446,32 +501,77 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( let refresh_only = payload.refresh_only; if refresh_only { } else { - provision_sandbox_users( + let provision_result = provision_sandbox_users( &payload.codex_home, &payload.offline_username, &payload.online_username, log, - )?; + ); + if let Err(err) = provision_result { + if extract_setup_failure(&err).is_some() { + return Err(err); + } + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUserProvisionFailed, + format!("provision sandbox users failed: {err}"), + ))); + } let users = vec![ payload.offline_username.clone(), payload.online_username.clone(), ]; hide_newly_created_users(&users, sbx_dir); } - let offline_sid = resolve_sid(&payload.offline_username)?; + let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSidResolveFailed, + format!( + "resolve SID for offline user {} failed: {err}", + payload.offline_username + ), + )) + })?; let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?; - let sandbox_group_sid = resolve_sandbox_users_group_sid()?; - let sandbox_group_psid = sid_bytes_to_psid(&sandbox_group_sid)?; + let sandbox_group_sid = resolve_sandbox_users_group_sid().map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSidResolveFailed, + format!("resolve sandbox users group SID failed: {err}"), + )) + })?; + let sandbox_group_psid = sid_bytes_to_psid(&sandbox_group_sid).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSidResolveFailed, + format!("convert sandbox users group SID to PSID failed: {err}"), + )) + })?; - let caps = load_or_create_cap_sids(&payload.codex_home)?; + let caps = load_or_create_cap_sids(&payload.codex_home).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperCapabilitySidFailed, + format!("load or create capability SIDs failed: {err}"), + )) + })?; let cap_psid = unsafe { - convert_string_sid_to_sid(&caps.workspace) - .ok_or_else(|| anyhow::anyhow!("convert capability SID failed"))? + convert_string_sid_to_sid(&caps.workspace).ok_or_else(|| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperCapabilitySidFailed, + format!("convert capability SID {} failed", caps.workspace), + )) + })? }; let mut refresh_errors: Vec = Vec::new(); if !refresh_only { - firewall::ensure_offline_outbound_block(&offline_sid_str, log)?; + let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log); + if let Err(err) = firewall_result { + if extract_setup_failure(&err).is_some() { + return Err(err); + } + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("ensure offline outbound block failed: {err}"), + ))); + } } if payload.read_roots.is_empty() { @@ -482,14 +582,26 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( log_line(log, "read ACL helper already running; skipping spawn")?; } Ok(false) => { - spawn_read_acl_helper(payload, log)?; + spawn_read_acl_helper(payload, log).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperReadAclHelperSpawnFailed, + format!("spawn read ACL helper failed: {err}"), + )) + })?; } Err(err) => { log_line( log, &format!("read ACL mutex check failed: {err}; spawning anyway"), )?; - spawn_read_acl_helper(payload, log)?; + spawn_read_acl_helper(payload, log).map_err(|spawn_err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperReadAclHelperSpawnFailed, + format!( + "spawn read ACL helper failed after mutex error {err}: {spawn_err}" + ), + )) + })?; } } } @@ -615,14 +727,32 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<( &sandbox_group_sid, GRANT_ACCESS, log, - )?; + ) + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxLockFailed, + format!( + "lock sandbox dir {} failed: {err}", + sandbox_dir(&payload.codex_home).display() + ), + )) + })?; lock_sandbox_dir( &sandbox_secrets_dir(&payload.codex_home), &payload.real_user, &sandbox_group_sid, DENY_ACCESS, log, - )?; + ) + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxLockFailed, + format!( + "lock sandbox secrets dir {} failed: {err}", + sandbox_secrets_dir(&payload.codex_home).display() + ), + )) + })?; let legacy_users = sandbox_dir(&payload.codex_home).join("sandbox_users.json"); if legacy_users.exists() { let _ = std::fs::remove_file(&legacy_users); diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 19e20fc9d0..74137379ec 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -13,6 +13,11 @@ use crate::allow::compute_allow_paths; use crate::allow::AllowDenyPaths; use crate::logging::log_note; use crate::policy::SandboxPolicy; +use crate::setup_error::clear_setup_error_report; +use crate::setup_error::failure; +use crate::setup_error::read_setup_error_report; +use crate::setup_error::SetupErrorCode; +use crate::setup_error::SetupFailure; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; @@ -308,7 +313,30 @@ fn find_setup_exe() -> PathBuf { PathBuf::from("codex-windows-sandbox-setup.exe") } -fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<()> { +fn report_helper_failure( + codex_home: &Path, + cleared_report: bool, + exit_code: Option, +) -> anyhow::Error { + let exit_detail = format!("setup helper exited with status {exit_code:?}"); + if !cleared_report { + return failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail); + } + match read_setup_error_report(codex_home) { + Ok(Some(report)) => anyhow::Error::new(SetupFailure::from_report(report)), + Ok(None) => failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail), + Err(err) => failure( + SetupErrorCode::OrchestratorHelperReportReadFailed, + format!("{exit_detail}; failed to read setup_error.json: {err}"), + ), + } +} + +fn run_setup_exe( + payload: &ElevationPayload, + needs_elevation: bool, + codex_home: &Path, +) -> Result<()> { use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::WaitForSingleObject; use windows_sys::Win32::System::Threading::INFINITE; @@ -316,8 +344,25 @@ fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<() use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS; use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW; let exe = find_setup_exe(); - let payload_json = serde_json::to_string(payload)?; + let payload_json = serde_json::to_string(payload).map_err(|err| { + failure( + SetupErrorCode::OrchestratorPayloadSerializeFailed, + format!("failed to serialize elevation payload: {err}"), + ) + })?; let payload_b64 = BASE64_STANDARD.encode(payload_json.as_bytes()); + let cleared_report = match clear_setup_error_report(codex_home) { + Ok(()) => true, + Err(err) => { + log_note( + &format!( + "setup orchestrator: failed to clear setup_error.json before launch: {err}" + ), + Some(&sandbox_dir(codex_home)), + ); + false + } + }; if !needs_elevation { let status = Command::new(&exe) @@ -327,13 +372,27 @@ fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<() .stdout(Stdio::null()) .stderr(Stdio::null()) .status() - .context("failed to launch setup helper")?; + .map_err(|err| { + failure( + SetupErrorCode::OrchestratorHelperLaunchFailed, + format!("failed to launch setup helper (non-elevated): {err}"), + ) + })?; if !status.success() { - return Err(anyhow!( - "setup helper exited with status {:?}", - status.code() + return Err(report_helper_failure( + codex_home, + cleared_report, + status.code(), )); } + if let Err(err) = clear_setup_error_report(codex_home) { + log_note( + &format!( + "setup orchestrator: failed to clear setup_error.json after success: {err}" + ), + Some(&sandbox_dir(codex_home)), + ); + } return Ok(()); } @@ -351,9 +410,10 @@ fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<() sei.nShow = 0; // SW_HIDE let ok = unsafe { ShellExecuteExW(&mut sei) }; if ok == 0 || sei.hProcess == 0 { - return Err(anyhow!( - "ShellExecuteExW failed to launch setup helper: {}", - unsafe { GetLastError() } + let last_error = unsafe { GetLastError() }; + return Err(failure( + SetupErrorCode::OrchestratorHelperLaunchFailed, + format!("ShellExecuteExW failed to launch setup helper: {last_error}"), )); } unsafe { @@ -362,9 +422,19 @@ fn run_setup_exe(payload: &ElevationPayload, needs_elevation: bool) -> Result<() GetExitCodeProcess(sei.hProcess, &mut code); CloseHandle(sei.hProcess); if code != 0 { - return Err(anyhow!("setup helper exited with status {}", code)); + return Err(report_helper_failure( + codex_home, + cleared_report, + Some(code as i32), + )); } } + if let Err(err) = clear_setup_error_report(codex_home) { + log_note( + &format!("setup orchestrator: failed to clear setup_error.json after success: {err}"), + Some(&sandbox_dir(codex_home)), + ); + } Ok(()) } @@ -379,7 +449,12 @@ pub fn run_elevated_setup( ) -> Result<()> { // Ensure the shared sandbox directory exists before we send it to the elevated helper. let sbx_dir = sandbox_dir(codex_home); - std::fs::create_dir_all(&sbx_dir)?; + std::fs::create_dir_all(&sbx_dir).map_err(|err| { + failure( + SetupErrorCode::OrchestratorSandboxDirCreateFailed, + format!("failed to create sandbox dir {}: {err}", sbx_dir.display()), + ) + })?; let (read_roots, write_roots) = build_payload_roots( policy, policy_cwd, @@ -399,8 +474,13 @@ pub fn run_elevated_setup( real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), refresh_only: false, }; - let needs_elevation = !is_elevated()?; - run_setup_exe(&payload, needs_elevation) + let needs_elevation = !is_elevated().map_err(|err| { + failure( + SetupErrorCode::OrchestratorElevationCheckFailed, + format!("failed to determine elevation state: {err}"), + ) + })?; + run_setup_exe(&payload, needs_elevation, codex_home) } fn build_payload_roots( diff --git a/docs/tui-chat-composer.md b/docs/tui-chat-composer.md index baab4ce0ed..01b5334d94 100644 --- a/docs/tui-chat-composer.md +++ b/docs/tui-chat-composer.md @@ -49,6 +49,19 @@ The solution is to detect paste-like _bursts_ and buffer them into a single expl - After handling the key, `sync_popups()` runs so popup visibility/filters stay consistent with the latest text + cursor. +### History navigation (↑/↓) + +Up/Down recall is handled by `ChatComposerHistory` and merges two sources: + +- **Persistent history** (cross-session, fetched from `~/.codex/history.jsonl`): text-only. It + does **not** carry text element ranges or local image attachments, so recalling one of these + entries only restores the text. +- **Local history** (current session): stores the full submission payload, including text + elements and local image paths. Recalling a local entry rehydrates placeholders and attachments. + +This distinction keeps the on-disk history backward compatible and avoids persisting attachments, +while still providing a richer recall experience for in-session edits. + ## Config gating for reuse `ChatComposer` now supports feature gating via `ChatComposerConfig` diff --git a/package.json b/package.json index ae676d8230..9f1452a972 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,5 @@ "node": ">=22", "pnpm": ">=9.0.0" }, - "packageManager": "pnpm@10.8.1" + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ddd5674bf1..78899b9321 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,7 @@ packages: ignoredBuiltDependencies: - esbuild + +minimumReleaseAge: 10080 + +blockExoticSubdeps: true diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 09e8a513d3..ae87b63fab 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -131,3 +131,19 @@ const codex = new Codex({ The SDK still injects its required variables (such as `OPENAI_BASE_URL` and `CODEX_API_KEY`) on top of the environment you provide. + +### Passing `--config` overrides + +Use the `config` option to provide additional Codex CLI configuration overrides. The SDK accepts a JSON object, flattens it +into dotted paths, and serializes values as TOML literals before passing them as repeated `--config key=value` flags. + +```typescript +const codex = new Codex({ + config: { + show_raw_agent_reasoning: true, + sandbox_workspace_write: { network_access: true }, + }, +}); +``` + +Thread options still take precedence for overlapping settings because they are emitted after these global overrides. diff --git a/sdk/typescript/src/codex.ts b/sdk/typescript/src/codex.ts index a42159232e..e3ce4aa0de 100644 --- a/sdk/typescript/src/codex.ts +++ b/sdk/typescript/src/codex.ts @@ -13,7 +13,8 @@ export class Codex { private options: CodexOptions; constructor(options: CodexOptions = {}) { - this.exec = new CodexExec(options.codexPathOverride, options.env); + const { codexPathOverride, env, config } = options; + this.exec = new CodexExec(codexPathOverride, env, config); this.options = options; } diff --git a/sdk/typescript/src/codexOptions.ts b/sdk/typescript/src/codexOptions.ts index 31fb637d4c..b6abb94b79 100644 --- a/sdk/typescript/src/codexOptions.ts +++ b/sdk/typescript/src/codexOptions.ts @@ -1,7 +1,19 @@ +export type CodexConfigValue = string | number | boolean | CodexConfigValue[] | CodexConfigObject; + +export type CodexConfigObject = { [key: string]: CodexConfigValue }; + export type CodexOptions = { codexPathOverride?: string; baseUrl?: string; apiKey?: string; + /** + * Additional `--config key=value` overrides to pass to the Codex CLI. + * + * Provide a JSON object and the SDK will flatten it into dotted paths and + * serialize values as TOML literals so they are compatible with the CLI's + * `--config` parsing. + */ + config?: CodexConfigObject; /** * Environment variables passed to the Codex CLI process. When provided, the SDK * will not inherit variables from `process.env`. diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index c8e957e1f2..d569106c84 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -3,12 +3,8 @@ import path from "node:path"; import readline from "node:readline"; import { fileURLToPath } from "node:url"; -import { - SandboxMode, - ModelReasoningEffort, - ApprovalMode, - WebSearchMode, -} from "./threadOptions"; +import type { CodexConfigObject, CodexConfigValue } from "./codexOptions"; +import { SandboxMode, ModelReasoningEffort, ApprovalMode, WebSearchMode } from "./threadOptions"; export type CodexExecArgs = { input: string; @@ -49,15 +45,27 @@ const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts"; export class CodexExec { private executablePath: string; private envOverride?: Record; + private configOverrides?: CodexConfigObject; - constructor(executablePath: string | null = null, env?: Record) { + constructor( + executablePath: string | null = null, + env?: Record, + configOverrides?: CodexConfigObject, + ) { this.executablePath = executablePath || findCodexPath(); this.envOverride = env; + this.configOverrides = configOverrides; } async *run(args: CodexExecArgs): AsyncGenerator { const commandArgs: string[] = ["exec", "--experimental-json"]; + if (this.configOverrides) { + for (const override of serializeConfigOverrides(this.configOverrides)) { + commandArgs.push("--config", override); + } + } + if (args.model) { commandArgs.push("--model", args.model); } @@ -202,6 +210,94 @@ export class CodexExec { } } +function serializeConfigOverrides(configOverrides: CodexConfigObject): string[] { + const overrides: string[] = []; + flattenConfigOverrides(configOverrides, "", overrides); + return overrides; +} + +function flattenConfigOverrides( + value: CodexConfigValue, + prefix: string, + overrides: string[], +): void { + if (!isPlainObject(value)) { + if (prefix) { + overrides.push(`${prefix}=${toTomlValue(value, prefix)}`); + return; + } else { + throw new Error("Codex config overrides must be a plain object"); + } + } + + const entries = Object.entries(value); + if (!prefix && entries.length === 0) { + return; + } + + if (prefix && entries.length === 0) { + overrides.push(`${prefix}={}`); + return; + } + + for (const [key, child] of entries) { + if (!key) { + throw new Error("Codex config override keys must be non-empty strings"); + } + if (child === undefined) { + continue; + } + const path = prefix ? `${prefix}.${key}` : key; + if (isPlainObject(child)) { + flattenConfigOverrides(child, path, overrides); + } else { + overrides.push(`${path}=${toTomlValue(child, path)}`); + } + } +} + +function toTomlValue(value: CodexConfigValue, path: string): string { + if (typeof value === "string") { + return JSON.stringify(value); + } else if (typeof value === "number") { + if (!Number.isFinite(value)) { + throw new Error(`Codex config override at ${path} must be a finite number`); + } + return `${value}`; + } else if (typeof value === "boolean") { + return value ? "true" : "false"; + } else if (Array.isArray(value)) { + const rendered = value.map((item, index) => toTomlValue(item, `${path}[${index}]`)); + return `[${rendered.join(", ")}]`; + } else if (isPlainObject(value)) { + const parts: string[] = []; + for (const [key, child] of Object.entries(value)) { + if (!key) { + throw new Error("Codex config override keys must be non-empty strings"); + } + if (child === undefined) { + continue; + } + parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path}.${key}`)}`); + } + return `{${parts.join(", ")}}`; + } else if (value === null) { + throw new Error(`Codex config override at ${path} cannot be null`); + } else { + const typeName = typeof value; + throw new Error(`Unsupported Codex config override value at ${path}: ${typeName}`); + } +} + +const TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/; +function formatTomlKey(key: string): string { + return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key); +} + +function isPlainObject(value: unknown): value is CodexConfigObject { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + const scriptFileName = fileURLToPath(import.meta.url); const scriptDirName = path.dirname(scriptFileName); diff --git a/sdk/typescript/tests/run.test.ts b/sdk/typescript/tests/run.test.ts index 993b2304d5..410bf50267 100644 --- a/sdk/typescript/tests/run.test.ts +++ b/sdk/typescript/tests/run.test.ts @@ -410,6 +410,86 @@ describe("Codex", () => { } }); + it("passes CodexOptions config overrides as TOML --config flags", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("Config overrides applied", "item_1"), + responseCompleted("response_1"), + ), + ], + }); + + const { args: spawnArgs, restore } = codexExecSpy(); + + try { + const client = new Codex({ + codexPathOverride: codexExecPath, + baseUrl: url, + apiKey: "test", + config: { + approval_policy: "never", + sandbox_workspace_write: { network_access: true }, + retry_budget: 3, + tool_rules: { allow: ["git status", "git diff"] }, + }, + }); + + const thread = client.startThread(); + await thread.run("apply config overrides"); + + const commandArgs = spawnArgs[0]; + expect(commandArgs).toBeDefined(); + expectPair(commandArgs, ["--config", 'approval_policy="never"']); + expectPair(commandArgs, ["--config", "sandbox_workspace_write.network_access=true"]); + expectPair(commandArgs, ["--config", "retry_budget=3"]); + expectPair(commandArgs, ["--config", 'tool_rules.allow=["git status", "git diff"]']); + } finally { + restore(); + await close(); + } + }); + + it("lets thread options override CodexOptions config overrides", async () => { + const { url, close } = await startResponsesTestProxy({ + statusCode: 200, + responseBodies: [ + sse( + responseStarted("response_1"), + assistantMessage("Thread overrides applied", "item_1"), + responseCompleted("response_1"), + ), + ], + }); + + const { args: spawnArgs, restore } = codexExecSpy(); + + try { + const client = new Codex({ + codexPathOverride: codexExecPath, + baseUrl: url, + apiKey: "test", + config: { approval_policy: "never" }, + }); + + const thread = client.startThread({ approvalPolicy: "on-request" }); + await thread.run("override approval policy"); + + const commandArgs = spawnArgs[0]; + const approvalPolicyOverrides = collectConfigValues(commandArgs, "approval_policy"); + expect(approvalPolicyOverrides).toEqual([ + 'approval_policy="never"', + 'approval_policy="on-request"', + ]); + expect(approvalPolicyOverrides.at(-1)).toBe('approval_policy="on-request"'); + } finally { + restore(); + await close(); + } + }); + it("allows overriding the env passed to the Codex CLI", async () => { const { url, close } = await startResponsesTestProxy({ statusCode: 200, @@ -737,13 +817,37 @@ describe("Codex", () => { } }, 10000); // TODO(pakrym): remove timeout }); + +/** + * Given a list of args to `codex` and a `key`, collects all `--config` + * overrides for that key. + */ +function collectConfigValues(args: string[] | undefined, key: string): string[] { + if (!args) { + throw new Error("args is undefined"); + } + + const values: string[] = []; + for (let i = 0; i < args.length; i += 1) { + if (args[i] !== "--config") { + continue; + } + + const override = args[i + 1]; + if (override?.startsWith(`${key}=`)) { + values.push(override); + } + } + return values; +} + function expectPair(args: string[] | undefined, pair: [string, string]) { if (!args) { - throw new Error("Args is undefined"); + throw new Error("args is undefined"); } - const index = args.indexOf(pair[0]); + const index = args.findIndex((arg, i) => arg === pair[0] && args[i + 1] === pair[1]); if (index === -1) { - throw new Error(`Pair ${pair[0]} not found in args`); + throw new Error(`Pair ${pair[0]} ${pair[1]} not found in args`); } expect(args[index + 1]).toBe(pair[1]); } diff --git a/shell-tool-mcp/package.json b/shell-tool-mcp/package.json index d27c0a0c59..24798b2f6e 100644 --- a/shell-tool-mcp/package.json +++ b/shell-tool-mcp/package.json @@ -36,5 +36,6 @@ "ts-jest": "^29.3.4", "tsup": "^8.5.0", "typescript": "^5.9.2" - } + }, + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264" }