diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml index f67f1983f2..4595a1fa65 100644 --- a/.codex/environments/environment.toml +++ b/.codex/environments/environment.toml @@ -8,4 +8,4 @@ script = "" [[actions]] name = "Run" icon = "run" -command = "cargo +1.93.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file" +command = "cargo +1.95.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file" diff --git a/.devcontainer/Dockerfile.secure b/.devcontainer/Dockerfile.secure index 6c1878eafa..83af317d8b 100644 --- a/.devcontainer/Dockerfile.secure +++ b/.devcontainer/Dockerfile.secure @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 ARG TZ ARG DEBIAN_FRONTEND=noninteractive ARG NODE_MAJOR=22 -ARG RUST_TOOLCHAIN=1.92.0 +ARG RUST_TOOLCHAIN=1.95.0 # Keep this in sync with .devcontainer/codex-install/package.json and pnpm-lock.yaml. ARG CODEX_NPM_VERSION=0.121.0 diff --git a/.devcontainer/devcontainer.secure.json b/.devcontainer/devcontainer.secure.json index 5d5808e541..7b05fb9a6c 100644 --- a/.devcontainer/devcontainer.secure.json +++ b/.devcontainer/devcontainer.secure.json @@ -7,7 +7,7 @@ "args": { "TZ": "${localEnv:TZ:UTC}", "NODE_MAJOR": "22", - "RUST_TOOLCHAIN": "1.92.0", + "RUST_TOOLCHAIN": "1.95.0", "CODEX_NPM_VERSION": "0.121.0" } }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 322aad46f4..668897c48e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,7 @@ # Core crate ownership. /codex-rs/core/ @openai/codex-core-agent-team /codex-rs/ext/extension-api/ @openai/codex-core-agent-team +/codex-rs/prompts/ @openai/codex-core-agent-team # Keep ownership changes reviewed by the same team. /.github/CODEOWNERS @openai/codex-core-agent-team diff --git a/.github/scripts/archive-release-symbols-and-strip-binaries.sh b/.github/scripts/archive-release-symbols-and-strip-binaries.sh new file mode 100755 index 0000000000..3e5894bb99 --- /dev/null +++ b/.github/scripts/archive-release-symbols-and-strip-binaries.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: archive-release-symbols-and-strip-binaries.sh \ + --target \ + --artifact-name \ + --release-dir \ + --archive-dir \ + --binaries "" +EOF +} + +target="" +artifact_name="" +release_dir="" +archive_dir="" +binaries="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --target) + target="${2:?--target requires a value}" + shift 2 + ;; + --artifact-name) + artifact_name="${2:?--artifact-name requires a value}" + shift 2 + ;; + --release-dir) + release_dir="${2:?--release-dir requires a value}" + shift 2 + ;; + --archive-dir) + archive_dir="${2:?--archive-dir requires a value}" + shift 2 + ;; + --binaries) + binaries="${2:?--binaries requires a value}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unexpected argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$target" || -z "$artifact_name" || -z "$release_dir" || -z "$archive_dir" || -z "$binaries" ]]; then + usage >&2 + exit 1 +fi + +symbols_root="${RUNNER_TEMP:-/tmp}/codex-symbols-${artifact_name}" +symbols_dir="${symbols_root}/codex-symbols-${artifact_name}" +archive_path="${archive_dir%/}/codex-symbols-${artifact_name}.tar.gz" +rm -rf "$symbols_root" +mkdir -p "$symbols_dir" "$archive_dir" +read -r -a binary_names <<< "$binaries" + +case "$target" in + *apple-darwin) + for binary in "${binary_names[@]}"; do + binary_path="${release_dir%/}/${binary}" + dsym_path="${binary_path}.dSYM" + if [[ ! -f "$binary_path" ]]; then + echo "Binary $binary_path not found" >&2 + exit 1 + fi + if [[ ! -d "$dsym_path" ]]; then + echo "dSYM $dsym_path not found" >&2 + exit 1 + fi + + cp -RL "$dsym_path" "${symbols_dir}/${binary}.dSYM" + strip -S -x "$binary_path" + done + ;; + *linux*) + objcopy_bin="${OBJCOPY:-objcopy}" + strip_bin="${STRIP:-strip}" + for binary in "${binary_names[@]}"; do + binary_path="${release_dir%/}/${binary}" + debug_path="${symbols_dir}/${binary}.debug" + if [[ ! -f "$binary_path" ]]; then + echo "Binary $binary_path not found" >&2 + exit 1 + fi + + "$objcopy_bin" --only-keep-debug "$binary_path" "$debug_path" + "$strip_bin" --strip-debug --strip-unneeded "$binary_path" + "$objcopy_bin" --add-gnu-debuglink="$debug_path" "$binary_path" + done + ;; + *windows*) + for binary in "${binary_names[@]}"; do + pdb_path="${release_dir%/}/${binary}.pdb" + if [[ ! -f "$pdb_path" ]]; then + echo "PDB $pdb_path not found" >&2 + exit 1 + fi + + cp "$pdb_path" "${symbols_dir}/${binary}.pdb" + done + ;; + *) + echo "No symbols packaging support for target: $target" >&2 + exit 1 + ;; +esac + +rm -f "$archive_path" +tar -C "$symbols_root" -czf "$archive_path" "codex-symbols-${artifact_name}" diff --git a/.github/scripts/install-musl-build-tools.sh b/.github/scripts/install-musl-build-tools.sh index e4c6683d0e..49035f5391 100644 --- a/.github/scripts/install-musl-build-tools.sh +++ b/.github/scripts/install-musl-build-tools.sh @@ -150,7 +150,9 @@ for arg in "\$@"; do args+=("\${arg}") done -exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}" +# Zig enables UBSan for debug C builds by default. Rust links these objects +# without Zig's sanitizer runtime, so keep native dependencies uninstrumented. +exec "${zig_bin}" cc -target "${zig_target}" "\${args[@]}" -fno-sanitize=undefined EOF cat >"${cxx}" <> "$GITHUB_ENV" pkg_config_path_var="PKG_CONFIG_PATH_${TARGET}" pkg_config_path_var="${pkg_config_path_var//-/_}" echo "${pkg_config_path_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV" +pkg_config_libdir_var="PKG_CONFIG_LIBDIR_${TARGET}" +pkg_config_libdir_var="${pkg_config_libdir_var//-/_}" +# Do not let musl cross-builds resolve native libraries from the host glibc +# pkg-config directories. libcap is the only target package provided here. +echo "${pkg_config_libdir_var}=${libcap_pkgconfig_dir}" >> "$GITHUB_ENV" if [[ -n "${sysroot}" && "${sysroot}" != "/" ]]; then echo "PKG_CONFIG_SYSROOT_DIR=${sysroot}" >> "$GITHUB_ENV" diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 11c0988ceb..153ace0fc9 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -141,7 +141,9 @@ jobs: - 2 - 3 - 4 - runs-on: windows-latest + runs-on: + group: codex-runners + labels: codex-windows-x64 name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4 steps: @@ -246,7 +248,9 @@ jobs: # it a larger timeout. if: github.event_name == 'push' && github.ref == 'refs/heads/main' timeout-minutes: 40 - runs-on: windows-latest + runs-on: + group: codex-runners + labels: codex-windows-x64 name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main) steps: @@ -332,7 +336,10 @@ jobs: target: aarch64-apple-darwin - os: windows-latest target: x86_64-pc-windows-gnullvm - runs-on: ${{ matrix.os }} + runs_on: + group: codex-runners + labels: codex-windows-x64 + runs-on: ${{ matrix.runs_on || matrix.os }} name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }} steps: @@ -422,7 +429,10 @@ jobs: target: aarch64-apple-darwin - os: windows-latest target: x86_64-pc-windows-gnullvm - runs-on: ${{ matrix.os }} + runs_on: + group: codex-runners + labels: codex-windows-x64 + runs-on: ${{ matrix.runs_on || matrix.os }} name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }} steps: diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index f20d09e112..656c50ef28 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -20,10 +20,10 @@ jobs: persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Run cargo-deny uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2 with: - rust-version: 1.93.0 + rust-version: 1.95.0 manifest-path: ./codex-rs/Cargo.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1ee1395e1..9adc90e040 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,5 +74,15 @@ jobs: - name: Check root README ToC run: python3 scripts/readme_toc.py README.md + - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 + with: + tool: just + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: "0.11.3" + - name: Ruff format Python scripts (run `just fmt` to fix) + run: just fmt-scripts-check + - name: Prettier (run `pnpm run format:fix` to fix) run: pnpm run format diff --git a/.github/workflows/issue-deduplicator.yml b/.github/workflows/issue-deduplicator.yml index f15c190102..fea6348c46 100644 --- a/.github/workflows/issue-deduplicator.yml +++ b/.github/workflows/issue-deduplicator.yml @@ -12,6 +12,7 @@ jobs: # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes) if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate')) runs-on: ubuntu-latest + environment: issue-triage permissions: contents: read outputs: @@ -157,6 +158,7 @@ jobs: needs: normalize-duplicates-all if: ${{ needs.normalize-duplicates-all.result == 'success' && needs.normalize-duplicates-all.outputs.has_matches != 'true' }} runs-on: ubuntu-latest + environment: issue-triage permissions: contents: read outputs: diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index 77fe5d07c8..2c4eb6aa68 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -12,6 +12,7 @@ jobs: # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes) if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-label')) runs-on: ubuntu-latest + environment: issue-triage permissions: contents: read outputs: diff --git a/.github/workflows/python-sdk-release.yml b/.github/workflows/python-sdk-release.yml new file mode 100644 index 0000000000..654dd8d822 --- /dev/null +++ b/.github/workflows/python-sdk-release.yml @@ -0,0 +1,91 @@ +name: python-sdk-release + +on: + push: + tags: + - "python-v*" + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + build-python-sdk: + if: github.repository == 'openai/codex' + name: build-python-sdk + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Validate tag and build Python SDK package + shell: bash + run: | + set -euo pipefail + + sdk_version="${GITHUB_REF_NAME#python-v}" + if [[ ! "${sdk_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then + echo "Python SDK release tags must identify a beta release, for example python-v0.1.0b1." + exit 1 + fi + + # The pinned runtime currently publishes a musllinux Linux wheel. + # Build in Alpine so release type generation installs that wheel. + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e HOME=/tmp/codex-python-sdk-home \ + -e UV_LINK_MODE=copy \ + -e SDK_VERSION="${sdk_version}" \ + -e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \ + -e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \ + -v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + -v "${RUNNER_TEMP}:${RUNNER_TEMP}" \ + -w "${GITHUB_WORKSPACE}/sdk/python" \ + python:3.12-alpine \ + sh -euxc ' + python -m venv /tmp/release-tools + /tmp/release-tools/bin/python -m pip install build twine uv==0.11.3 + /tmp/release-tools/bin/uv sync --extra dev --frozen + /tmp/release-tools/bin/uv run --extra dev --frozen python scripts/update_sdk_artifacts.py \ + stage-sdk "${SDK_STAGE_DIR}" \ + --sdk-version "${SDK_VERSION}" + /tmp/release-tools/bin/python -m build \ + --wheel \ + --sdist \ + --outdir "${SDK_DIST_DIR}" \ + "${SDK_STAGE_DIR}" + /tmp/release-tools/bin/python -m twine check --strict "${SDK_DIST_DIR}/"* + ' + + - name: Upload Python SDK package + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: python-sdk-package + path: dist/python-sdk/* + if-no-files-found: error + + publish-python-sdk: + name: publish-python-sdk + needs: build-python-sdk + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: read + id-token: write # Required for PyPI trusted publishing. + + steps: + - name: Download Python SDK package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-sdk-package + path: dist/python-sdk + + - name: Publish Python SDK to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + packages-dir: dist/python-sdk diff --git a/.github/workflows/rust-ci-full-nextest-platform.yml b/.github/workflows/rust-ci-full-nextest-platform.yml index 7dc39d33ed..3fdf7b51ee 100644 --- a/.github/workflows/rust-ci-full-nextest-platform.yml +++ b/.github/workflows/rust-ci-full-nextest-platform.yml @@ -94,7 +94,7 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ inputs.target }} @@ -319,7 +319,7 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ inputs.target }} diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml index c4a9329c00..60d508b21d 100644 --- a/.github/workflows/rust-ci-full.yml +++ b/.github/workflows/rust-ci-full.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: components: rustfmt - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: cargo-shear@1.11.2 @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: toolchain: nightly-2025-09-18 components: llvm-tools-preview, rustc-dev, rust-src @@ -260,13 +260,9 @@ jobs: set -euo pipefail if command -v apt-get >/dev/null 2>&1; then sudo apt-get update -y - packages=(pkg-config libcap-dev) - if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then - packages+=(libubsan1) - fi - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} components: clippy @@ -348,14 +344,6 @@ 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 @@ -389,58 +377,6 @@ 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" - - if: ${{ !contains(matrix.target, 'windows') }} name: Configure rusty_v8 artifact overrides and verify checksums uses: ./.github/actions/setup-rusty-v8 diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 029b6f2c7e..d1d7c0df96 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -67,7 +67,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: components: rustfmt - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 @@ -91,7 +91,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: cargo-shear@1.11.2 @@ -111,7 +111,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Install nightly argument-comment-lint toolchain shell: bash run: | diff --git a/.github/workflows/rust-release-argument-comment-lint.yml b/.github/workflows/rust-release-argument-comment-lint.yml index f654bd9dd7..94f15af7d4 100644 --- a/.github/workflows/rust-release-argument-comment-lint.yml +++ b/.github/workflows/rust-release-argument-comment-lint.yml @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: toolchain: nightly-2025-09-18 targets: ${{ matrix.target }} diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index 51412be0e0..91887cbe0b 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -34,6 +34,8 @@ jobs: working-directory: codex-rs env: CARGO_PROFILE_RELEASE_LTO: ${{ inputs.release-lto }} + CARGO_PROFILE_RELEASE_DEBUG: full + CARGO_PROFILE_RELEASE_STRIP: "false" strategy: fail-fast: false @@ -100,18 +102,22 @@ jobs: Write-Host "Total RAM: $ramGiB GiB" Write-Host "Disk usage:" Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}} - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} - name: Cargo build (Windows binaries) shell: bash run: | + target="${{ matrix.target }}" + if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then + export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC + fi build_args=() for binary in ${{ matrix.binaries }}; do build_args+=(--bin "$binary") done - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + cargo build --target "$target" --release --timings "${build_args[@]}" - name: Upload Cargo timings uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -127,6 +133,7 @@ jobs: mkdir -p "$output_dir" for binary in ${{ matrix.binaries }}; do cp "target/${{ matrix.target }}/release/${binary}.exe" "$output_dir/${binary}.exe" + cp "target/${{ matrix.target }}/release/${binary}.pdb" "$output_dir/${binary}.pdb" done - name: Upload Windows binaries @@ -209,6 +216,23 @@ jobs: account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }} + - name: Build symbols archive + shell: bash + run: | + bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \ + --target "${{ matrix.target }}" \ + --artifact-name "${{ matrix.target }}" \ + --release-dir "target/${{ matrix.target }}/release" \ + --archive-dir "symbols-dist/${{ matrix.target }}" \ + --binaries "${WINDOWS_BINARIES}" + + - name: Upload symbols archive + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ matrix.target }}-symbols + path: codex-rs/symbols-dist/${{ matrix.target }}/* + if-no-files-found: error + - name: Stage artifacts shell: bash run: | diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2cd11e66fe..364fe78902 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Validate tag matches Cargo.toml version shell: bash env: @@ -149,6 +149,14 @@ jobs: # 2026-03-04: temporarily change releases to use thin LTO because # Ubuntu ARM is timing out at 60 minutes. CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }} + CARGO_PROFILE_RELEASE_DEBUG: full + CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO: ${{ contains(matrix.target, 'apple-darwin') && 'packed' || 'off' }} + CARGO_PROFILE_RELEASE_STRIP: "false" + # Use the git CLI instead of Cargo's libgit2 path for git dependencies. + # macOS release runners have intermittently failed to fetch nested + # submodules through SecureTransport/libgit2, especially libwebrtc's + # libyuv submodule from chromium.googlesource.com. + CARGO_NET_GIT_FETCH_WITH_CLI: "true" SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} strategy: @@ -244,17 +252,8 @@ jobs: run: | set -euo pipefail sudo apt-get update -y - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev - - 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@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends binutils pkg-config libcap-dev + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} @@ -283,30 +282,7 @@ jobs: 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) + name: Disable aws-lc jitter entropy (musl) shell: bash run: | set -euo pipefail @@ -316,30 +292,6 @@ jobs: target_no_jitter="${target_no_jitter//-/_}" echo "${target_no_jitter}=1" >> "$GITHUB_ENV" - # Clear global Rust flags so host/proc-macro builds don't pull in UBSan. - echo "RUSTFLAGS=" >> "$GITHUB_ENV" - echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV" - 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: Configure rusty_v8 artifact overrides and verify checksums uses: ./.github/actions/setup-rusty-v8 with: @@ -359,6 +311,10 @@ jobs: exit 1 fi + # Codex embeds this digest at build time and verifies the bundled + # bwrap resource before use. Strip bwrap before hashing so the digest + # covers the exact bytes that the release packages. + strip --strip-debug --strip-unneeded "$bwrap_path" digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" echo "Built bwrap ${bwrap_path} with sha256:${digest}" @@ -366,12 +322,21 @@ jobs: - name: Cargo build shell: bash run: | + target="${{ matrix.target }}" + if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then + export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC + fi build_args=() for binary in ${{ matrix.binaries }}; do + # bwrap was built, finalized, and hashed before this build so + # Codex can embed the digest of the bytes that will be packaged. + if [[ "$binary" == "bwrap" ]]; then + continue + fi build_args+=(--bin "$binary") done echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" - cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" + cargo build --target "$target" --release --timings "${build_args[@]}" - name: Upload Cargo timings uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -380,6 +345,32 @@ jobs: path: codex-rs/target/**/cargo-timings/cargo-timing.html if-no-files-found: warn + - name: Build symbols archive and strip binaries + shell: bash + run: | + binaries=() + for binary in ${{ matrix.binaries }}; do + # bwrap is already stripped before hashing. Its symbols are not + # useful enough to justify a separate pre-Codex symbols pass. + if [[ "$binary" == "bwrap" ]]; then + continue + fi + binaries+=("$binary") + done + bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \ + --target "${{ matrix.target }}" \ + --artifact-name "${{ matrix.artifact_name }}" \ + --release-dir "target/${{ matrix.target }}/release" \ + --archive-dir "symbols-dist/${{ matrix.artifact_name }}" \ + --binaries "${binaries[*]}" + + - name: Upload symbols archive + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ matrix.artifact_name }}-symbols + path: codex-rs/symbols-dist/${{ matrix.artifact_name }}/* + if-no-files-found: error + - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} name: Stage unsigned macOS artifacts shell: bash @@ -1078,6 +1069,7 @@ jobs: run: | find dist -mindepth 1 -maxdepth 1 -type d \ ! -name '*-apple-darwin*-unsigned' \ + ! -name '*-symbols' \ ! -name 'aarch64-unknown-linux-musl' \ ! -name 'aarch64-unknown-linux-musl-app-server' \ ! -name 'x86_64-unknown-linux-musl' \ @@ -1282,19 +1274,6 @@ jobs: tag: ${{ github.ref_name }} config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy - # Only trigger the deploy if the release is not a pre-release. - # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} - continue-on-error: true - env: - DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} - run: | - if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then - echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}" - exit 1 - fi - # Publish to npm using OIDC authentication. # July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/ # npm docs: https://docs.npmjs.com/trusted-publishers @@ -1507,6 +1486,36 @@ jobs: packages-dir: dist/python-runtime skip-existing: true + deploy-dev-website: + name: Trigger developers.openai.com deploy + needs: release + # Only trigger the deploy for a stable signed release. + # The deploy updates developers.openai.com with the new config schema json file. + if: >- + ${{ + !cancelled() && + needs.release.result == 'success' && + needs.release.outputs.sign_macos == 'true' && + !contains(needs.release.outputs.version, '-') + }} + runs-on: ubuntu-latest + continue-on-error: true + permissions: {} + environment: + name: dev-website-vercel-deploy + deployment: false + + steps: + - name: Trigger developers.openai.com deploy + continue-on-error: true + env: + DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} + run: | + if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then + echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}" + exit 1 + fi + winget: name: winget needs: release diff --git a/.github/workflows/rusty-v8-release.yml b/.github/workflows/rusty-v8-release.yml index 3b56a1e93f..d98cdb2c43 100644 --- a/.github/workflows/rusty-v8-release.yml +++ b/.github/workflows/rusty-v8-release.yml @@ -152,9 +152,9 @@ jobs: python-version: "3.12" - name: Set up Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" - name: Build Bazel V8 release pair env: diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml index 979e991504..6341f218a6 100644 --- a/.github/workflows/v8-canary.yml +++ b/.github/workflows/v8-canary.yml @@ -166,9 +166,9 @@ jobs: python-version: "3.12" - name: Set up Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" - name: Build Bazel V8 release pair env: @@ -310,9 +310,9 @@ jobs: architecture: x64 - name: Set up Codex Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" targets: ${{ matrix.target }} - name: Install rusty_v8 Rust toolchain @@ -401,7 +401,7 @@ jobs: cd codex-rs RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \ RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \ - cargo +1.93.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run + cargo +1.95.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run ) - name: Upload staged artifacts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e92864a68a..5003c6de08 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "BazelBuild.vscode-bazel", "rust-lang.rust-analyzer", "charliermarsh.ruff", "tamasfe.even-better-toml", diff --git a/AGENTS.md b/AGENTS.md index 4714b1b8aa..63a9ba67c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ In the codex-rs folder where the rust code lives: trivial; prefer new modules/files and keep `chatwidget.rs` focused on orchestration. - When running Rust commands (e.g. `just fix` or `just test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected. -Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests: +Run `just fmt` (in the `codex-rs` directory) automatically after you have finished making code changes anywhere in this repository; do not ask for approval to run it. Additionally, run the tests: 1. Do not run `cargo test` directly. Use `just test` so test execution follows the repo defaults. 2. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `just test -p codex-tui`. @@ -110,6 +110,19 @@ See `codex-rs/tui/styles.md`. ## Tests +### Test module organization + +- When adding a new test module, define its contents in a separate sibling file rather than inline in the implementation file. +- Use an explicit `#[path = "..._tests.rs"]` attribute so the test filename is descriptive and easy to locate: + + ```rust + #[cfg(test)] + #[path = "parser_tests.rs"] + mod tests; + ``` + +- This applies only when introducing a new test module. Do not move or rewrite existing inline `#[cfg(test)] mod tests { ... }` modules solely to follow this convention. + ### Snapshot tests This repo uses snapshot tests (via `insta`), especially in `codex-rs/tui`, to validate rendered output. @@ -219,3 +232,12 @@ These guidelines apply to app-server protocol work in `codex-rs`, especially: - Validate with `just test -p codex-app-server-protocol`. - Avoid boilerplate tests that only assert experimental field markers for individual request fields in `common.rs`; rely on schema generation/tests and behavioral coverage instead. + +## Python Development Best Practices + +### Ignore Python 2 compatibility + +This project uses Python 3+. You should not use the `__future__` module. + +If you need to worry about feature compatibility between different 3.xx point releases, check the +closest `pyproject.toml`'s `requires-python` field to see what minimum runtime version is supported. diff --git a/MODULE.bazel b/MODULE.bazel index 81f16b7b9e..abca889d64 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -163,7 +163,7 @@ use_repo(nightly_rust, "rust_toolchains") toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains") toolchains.toolchain( edition = "2024", - version = "1.93.0", + version = "1.95.0", ) use_repo(toolchains, "default_rust_toolchains") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ad56196dd9..f541a2c7bf 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -704,6 +704,7 @@ "bitflags_2.11.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", "blake2_0.10.6": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"}],\"features\":{\"default\":[\"std\"],\"reset\":[],\"simd\":[],\"simd_asm\":[\"simd_opt\"],\"simd_opt\":[\"simd\"],\"size_opt\":[],\"std\":[\"digest/std\"]}}", "block-buffer_0.10.4": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{}}", + "block-buffer_0.12.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"name\":\"hybrid-array\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{}}", "block-padding_0.3.3": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{\"std\":[]}}", "block2_0.6.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"objc2\",\"req\":\">=0.6.2, <0.8.0\"}],\"features\":{\"alloc\":[],\"compiler-rt\":[\"objc2/unstable-compiler-rt\"],\"default\":[\"std\"],\"gnustep-1-7\":[\"objc2/gnustep-1-7\"],\"gnustep-1-8\":[\"gnustep-1-7\",\"objc2/gnustep-1-8\"],\"gnustep-1-9\":[\"gnustep-1-8\",\"objc2/gnustep-1-9\"],\"gnustep-2-0\":[\"gnustep-1-9\",\"objc2/gnustep-2-0\"],\"gnustep-2-1\":[\"gnustep-2-0\",\"objc2/gnustep-2-1\"],\"std\":[\"alloc\"],\"unstable-coerce-pointee\":[],\"unstable-objfw\":[],\"unstable-private\":[],\"unstable-winobjc\":[\"gnustep-1-8\"]}}", "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\":{}}", @@ -738,6 +739,7 @@ "cfg-if_1.0.4": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"rustc-dep-of-std\":[\"core\"]}}", "cfg_aliases_0.1.1": "{\"dependencies\":[],\"features\":{}}", "cfg_aliases_0.2.1": "{\"dependencies\":[],\"features\":{}}", + "chacha20_0.10.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"stream-wrapper\"],\"name\":\"cipher\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.5\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8.1\"}],\"features\":{\"default\":[\"cipher\"],\"legacy\":[\"cipher\"],\"rng\":[\"dep:rand_core\"],\"xchacha\":[\"cipher\"]}}", "chacha20_0.9.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"std\":[\"cipher/std\"],\"zeroize\":[\"cipher/zeroize\"]}}", "chacha20poly1305_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"req\":\"^0.9\"},{\"name\":\"cipher\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"reduced-round\":[],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "chardetng_0.1.17": "{\"dependencies\":[{\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"detone\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"encoding_rs\",\"req\":\"^0.8.29\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.2.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"multithreading\":[\"rayon\",\"arrayvec\"],\"testing-only-no-semver-guarantees-do-not-use\":[]}}", @@ -757,6 +759,7 @@ "clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}", "clru_0.6.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"req\":\"^0.16\"}],\"features\":{}}", "cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}", + "cmov_0.5.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.9\",\"target\":\"cfg(any(unix, windows))\"}],\"features\":{}}", "cmp_any_0.8.1": "{\"dependencies\":[],\"features\":{}}", "codespan-reporting_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.6.3\"},{\"kind\":\"dev\",\"name\":\"peg\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"pico-args\",\"req\":\"^0.5.0\"},{\"kind\":\"dev\",\"name\":\"rustyline\",\"req\":\"^6\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"termcolor\",\"optional\":true,\"req\":\"^1.0.4\"},{\"name\":\"unicode-width\",\"req\":\">=0.1, <0.3\"},{\"kind\":\"dev\",\"name\":\"unindent\",\"req\":\"^0.1\"}],\"features\":{\"ascii-only\":[],\"default\":[\"std\",\"termcolor\"],\"serialization\":[\"serde\"],\"std\":[\"serde?/std\"],\"termcolor\":[\"std\",\"dep:termcolor\"]}}", "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\":[]}}", @@ -789,6 +792,7 @@ "coreaudio-sys_0.2.17": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72\"}],\"features\":{\"audio_server_plugin\":[],\"audio_toolbox\":[],\"audio_unit\":[],\"core_audio\":[],\"core_midi\":[],\"default\":[\"audio_toolbox\",\"audio_unit\",\"core_audio\",\"audio_server_plugin\",\"open_al\",\"core_midi\"],\"io_kit_audio\":[],\"open_al\":[]}}", "cpal_0.15.3": "{\"dependencies\":[{\"name\":\"alsa\",\"req\":\"^0.9\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"asio-sys\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\",\"audio_toolbox\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"dasp_sample\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hound\",\"req\":\"^3.5\"},{\"name\":\"jack\",\"optional\":true,\"req\":\"^0.11\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"mach2\",\"req\":\"^0.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"name\":\"ndk\",\"req\":\"^0.8\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\"^0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.6\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"java-interface\"],\"name\":\"oboe\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ringbuf\",\"req\":\"^0.3\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.58\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.33\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"Win32_Media_Audio\",\"Win32_Foundation\",\"Win32_Devices_Properties\",\"Win32_Media_KernelStreaming\",\"Win32_System_Com_StructuredStorage\",\"Win32_System_Threading\",\"Win32_Security\",\"Win32_System_SystemServices\",\"Win32_System_Variant\",\"Win32_Media_Multimedia\",\"Win32_UI_Shell_PropertiesSystem\"],\"name\":\"windows\",\"req\":\"^0.54.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"asio\":[\"asio-sys\",\"num-traits\"],\"oboe-shared-stdcxx\":[\"oboe/shared-stdcxx\"]}}", "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\":{}}", + "cpufeatures_0.3.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"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\":{}}", @@ -802,6 +806,7 @@ "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", "crypto-bigint_0.5.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"rlp\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"serdect?/alloc\"],\"default\":[\"rand\"],\"extra-sizes\":[],\"rand\":[\"rand_core/std\"],\"serde\":[\"dep:serdect\"]}}", "crypto-common_0.1.7": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"=0.14.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", + "crypto-common_0.2.2": "{\"dependencies\":[{\"features\":[\"sys_rng\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"hybrid-array\",\"req\":\"^0.4.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"getrandom\":[\"rand_core\",\"dep:getrandom\"],\"rand_core\":[\"dep:rand_core\"],\"zeroize\":[\"hybrid-array/zeroize\"]}}", "crypto_box_0.9.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"blake2\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"crypto_secretbox\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rmp-serde\",\"req\":\"^1\"},{\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"chacha20\":[\"dep:chacha20\",\"crypto_secretbox/chacha20\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"salsa20\":[\"dep:salsa20\",\"crypto_secretbox/salsa20\"],\"seal\":[\"dep:blake2\",\"alloc\"],\"serde\":[\"dep:serdect\"],\"std\":[\"aead/std\"]}}", "crypto_secretbox_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"cipher\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"features\":[\"zeroize\"],\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "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\"]}}", @@ -809,6 +814,7 @@ "ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}", "ctor_0.1.26": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^1.0.98\"}],\"features\":{}}", "ctor_0.6.3": "{\"dependencies\":[{\"name\":\"ctor-proc-macro\",\"optional\":true,\"req\":\"=0.0.7\"},{\"default_features\":false,\"name\":\"dtor\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"}],\"features\":{\"__no_warn_on_missing_unsafe\":[\"dtor?/__no_warn_on_missing_unsafe\"],\"default\":[\"dtor\",\"proc_macro\",\"__no_warn_on_missing_unsafe\"],\"dtor\":[\"dep:dtor\"],\"proc_macro\":[\"dep:ctor-proc-macro\",\"dtor?/proc_macro\"],\"used_linker\":[\"dtor?/used_linker\"]}}", + "ctutils_0.4.2": "{\"dependencies\":[{\"name\":\"cmov\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.11\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"alloc\":[],\"subtle\":[\"dep:subtle\"]}}", "curve25519-dalek-derive_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.66\"},{\"name\":\"quote\",\"req\":\"^1.0.31\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.27\"}],\"features\":{}}", "curve25519-dalek_4.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2.6\",\"target\":\"cfg(target_arch = \\\"x86_64\\\")\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"curve25519-dalek-derive\",\"req\":\"^0.1\",\"target\":\"cfg(all(not(curve25519_dalek_backend = \\\"fiat\\\"), not(curve25519_dalek_backend = \\\"serial\\\"), target_arch = \\\"x86_64\\\"))\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"fiat-crypto\",\"req\":\"^0.2.1\",\"target\":\"cfg(curve25519_dalek_backend = \\\"fiat\\\")\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"group\":[\"dep:group\",\"rand_core\"],\"group-bits\":[\"group\",\"ff/bits\"],\"legacy_compatibility\":[],\"precomputed-tables\":[]}}", "cxx-build_1.0.194": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.0.101\"},{\"name\":\"codespan-reporting\",\"req\":\"^0.13.1\"},{\"kind\":\"dev\",\"name\":\"cxx\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"cxx-gen\",\"req\":\"^0.7\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"kind\":\"dev\",\"name\":\"pkg-config\",\"req\":\"^0.3.27\"},{\"default_features\":false,\"features\":[\"span-locations\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"scratch\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"full\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{\"parallel\":[\"cc/parallel\"]}}", @@ -850,6 +856,7 @@ "difflib_0.4.0": "{\"dependencies\":[],\"features\":{}}", "diffy_0.4.2": "{\"dependencies\":[{\"name\":\"nu-ansi-term\",\"req\":\"^0.50\"}],\"features\":{}}", "digest_0.10.7": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"block-buffer\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.4\"}],\"features\":{\"alloc\":[],\"core-api\":[\"block-buffer\"],\"default\":[\"core-api\"],\"dev\":[\"blobby\"],\"mac\":[\"subtle\"],\"oid\":[\"const-oid\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\"]}}", + "digest_0.11.3": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"block-buffer\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"common\",\"package\":\"crypto-common\",\"req\":\"^0.2\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"ctutils\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7\"}],\"features\":{\"alloc\":[],\"block-api\":[\"dep:block-buffer\"],\"default\":[\"block-api\"],\"dev\":[\"blobby\"],\"getrandom\":[\"common/getrandom\",\"rand_core\"],\"mac\":[\"dep:ctutils\"],\"oid\":[\"dep:const-oid\"],\"rand_core\":[\"common/rand_core\"],\"zeroize\":[\"dep:zeroize\",\"block-buffer?/zeroize\"]}}", "diplomat-runtime_0.15.1": "{\"dependencies\":[{\"name\":\"jni\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"}],\"features\":{\"jvm-callback-support\":[\"dep:jni\"],\"log\":[\"dep:log\"]}}", "diplomat_0.15.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"diplomat_core\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.30\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2.0\"}],\"features\":{}}", "diplomat_core_0.15.0": "{\"dependencies\":[{\"name\":\"displaydoc\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"either\",\"optional\":true,\"req\":\"^1.9.0\"},{\"features\":[\"yaml\"],\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"smallvec\",\"req\":\"^1.9.0\"},{\"features\":[\"ident\"],\"name\":\"strck\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"hir\":[\"either\"]}}", @@ -899,7 +906,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.14": "{\"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.62\",\"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\":{}}", + "etcetera_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Com\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"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.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"concurrent-queue\",\"req\":\"^2.4.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"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\"]}}", @@ -926,7 +933,6 @@ "fluent-langneg_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-locale\",\"req\":\"^0.9\"}],\"features\":{\"cldr\":[\"unic-langid/likelysubtags\"],\"default\":[]}}", "fluent-syntax_0.11.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"iai\",\"req\":\"^0.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"}],\"features\":{\"all-benchmarks\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"]}}", "fluent_0.16.1": "{\"dependencies\":[{\"name\":\"fluent-bundle\",\"req\":\"^0.15.3\"},{\"name\":\"fluent-pseudo\",\"optional\":true,\"req\":\"^0.3.2\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", - "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\":[]}}", @@ -938,20 +944,15 @@ "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-channel_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", - "futures-core_0.3.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-core_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-executor_0.3.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-executor_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.32\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\"]}}", "futures-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-io_0.3.32": "{\"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\":{}}", - "futures-sink_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "futures-macro_0.3.32": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", "futures-sink_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", - "futures-task_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-task_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-util_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", "futures-util_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.26\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"default_features\":false,\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.7\"},{\"name\":\"spin\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"slab\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\",\"libc\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\",\"libc\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab/std\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", "futures_0.3.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\":{}}", @@ -1048,7 +1049,7 @@ "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.5": "{\"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.1": "{\"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\"},{\"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\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(unix)\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"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\":[],\"serde\":[\"dep:serde_core\",\"dep:serde\"]}}", - "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\"]}}", + "hashlink_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"default-hasher\"],\"name\":\"hashbrown\",\"req\":\"^0.16\"},{\"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\"]}}", "headers-core_0.3.0": "{\"dependencies\":[{\"name\":\"http\",\"req\":\"^1.0.0\"}],\"features\":{}}", "headers_0.4.1": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"headers-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"httpdate\",\"req\":\"^1\"},{\"name\":\"mime\",\"req\":\"^0.3.14\"},{\"name\":\"sha1\",\"req\":\"^0.10\"}],\"features\":{\"nightly\":[]}}", "heapless_0.8.0": "{\"dependencies\":[{\"name\":\"defmt\",\"optional\":true,\"req\":\">=0.2.0, <0.4\"},{\"name\":\"hash32\",\"req\":\"^0.3.0\"},{\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"ufmt\",\"req\":\"^0.2\"},{\"name\":\"ufmt-write\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"defmt-03\":[\"dep:defmt\"],\"mpmc_large\":[],\"portable-atomic\":[\"dep:portable-atomic\"],\"portable-atomic-critical-section\":[\"dep:portable-atomic\",\"portable-atomic\",\"portable-atomic?/critical-section\"],\"portable-atomic-unsafe-assume-single-core\":[\"dep:portable-atomic\",\"portable-atomic\",\"portable-atomic?/unsafe-assume-single-core\"],\"serde\":[\"dep:serde\"],\"ufmt\":[\"dep:ufmt-write\"]}}", @@ -1059,7 +1060,9 @@ "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\"]}}", + "hkdf_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"name\":\"hmac\",\"req\":\"^0.13\"},{\"name\":\"kdf\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"}],\"features\":{}}", "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\"]}}", + "hmac_0.13.0": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"md-5\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.11\"}],\"features\":{\"zeroize\":[\"digest/zeroize\"]}}", "home_0.5.12": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "home_0.5.9": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "hostname_0.4.2": "{\"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\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.65\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"default\":[],\"set\":[]}}", @@ -1071,6 +1074,7 @@ "http_1.4.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httparse_1.10.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httpdate_1.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"}],\"features\":{}}", + "hybrid-array_0.4.12": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"ctutils\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"const-generics\"],\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"const-generics\"],\"name\":\"typenum\",\"req\":\"^1.20\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"}],\"features\":{\"alloc\":[],\"extra-sizes\":[]}}", "hyper-rustls_0.27.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"hyper\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"server-auto\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"tls12\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"native-tokio\",\"http1\",\"tls12\",\"logging\",\"aws-lc-rs\"],\"fips\":[\"aws-lc-rs\",\"rustls/fips\"],\"http1\":[\"hyper-util/http1\"],\"http2\":[\"hyper-util/http2\"],\"logging\":[\"log\",\"tokio-rustls/logging\",\"rustls/logging\"],\"native-tokio\":[\"rustls-native-certs\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"tokio-rustls/tls12\",\"rustls/tls12\"],\"webpki-tokio\":[\"webpki-roots\"]}}", "hyper-timeout_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"hyper\",\"req\":\"^1.1\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"hyper-tls\",\"req\":\"^0.6\"},{\"features\":[\"client-legacy\",\"http1\"],\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"features\":[\"client-legacy\",\"http1\",\"server\",\"server-graceful\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.35\"},{\"features\":[\"io-std\",\"io-util\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.35\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{}}", "hyper-tls_0.6.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"name\":\"native-tls\",\"req\":\"^0.2.1\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io-std\",\"macros\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"name\":\"tokio-native-tls\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{\"alpn\":[\"native-tls/alpn\"],\"vendored\":[\"native-tls/vendored\"]}}", @@ -1149,8 +1153,12 @@ "jiff-tzdb-platform_0.1.3": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"req\":\"^0.1.4\"}],\"features\":{}}", "jiff-tzdb_0.1.6": "{\"dependencies\":[],\"features\":{}}", "jiff_0.2.23": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.81\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"hifitime\",\"req\":\"^3.9.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"humantime\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.39.0\"},{\"name\":\"jiff-static\",\"req\":\"=0.2.23\",\"target\":\"cfg(any())\"},{\"name\":\"jiff-static\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.6\"},{\"name\":\"jiff-tzdb-platform\",\"optional\":true,\"req\":\"^0.1.3\",\"target\":\"cfg(any(windows, target_family = \\\"wasm\\\"))\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.50\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.21\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"req\":\"^1.10.0\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"name\":\"portable-atomic-util\",\"req\":\"^0.2.4\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.117\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.9.34\"},{\"kind\":\"dev\",\"name\":\"tabwriter\",\"req\":\"^1.4.0\"},{\"features\":[\"local-offset\",\"macros\",\"parsing\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.36\"},{\"kind\":\"dev\",\"name\":\"time-tz\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"tzfile\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.70\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_System_Time\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\">=0.52.0, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"portable-atomic-util/alloc\"],\"default\":[\"std\",\"tz-system\",\"tz-fat\",\"tzdb-bundle-platform\",\"tzdb-zoneinfo\",\"tzdb-concatenated\",\"perf-inline\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"logging\":[\"dep:log\"],\"perf-inline\":[],\"serde\":[\"dep:serde_core\"],\"static\":[\"static-tz\",\"jiff-static?/tzdb\"],\"static-tz\":[\"dep:jiff-static\"],\"std\":[\"alloc\",\"log?/std\",\"serde_core?/std\"],\"tz-fat\":[\"jiff-static?/tz-fat\"],\"tz-system\":[\"std\",\"dep:windows-sys\"],\"tzdb-bundle-always\":[\"dep:jiff-tzdb\",\"alloc\"],\"tzdb-bundle-platform\":[\"dep:jiff-tzdb-platform\",\"alloc\"],\"tzdb-concatenated\":[\"std\"],\"tzdb-zoneinfo\":[\"std\"]}}", + "jni-macros_0.22.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"javac\",\"req\":\"^0.1.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"name\":\"simd_cesu8\",\"req\":\"^1.0.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{}}", + "jni-sys-macros_0.4.1": "{\"dependencies\":[{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "jni-sys_0.3.0": "{\"dependencies\":[],\"features\":{}}", + "jni-sys_0.4.1": "{\"dependencies\":[{\"name\":\"jni-sys-macros\",\"req\":\"^0.4.1\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{}}", "jni_0.21.1": "{\"dependencies\":[{\"name\":\"cesu8\",\"req\":\"^1.1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"combine\",\"req\":\"^4.1.0\"},{\"name\":\"java-locator\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"jni-sys\",\"req\":\"^0.3.0\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"thiserror\",\"req\":\"^1.0.20\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"walkdir\",\"req\":\"^2\"},{\"features\":[\"Win32_Globalization\"],\"name\":\"windows-sys\",\"req\":\"^0.45.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"invocation\":[\"java-locator\",\"libloading\"]}}", + "jni_0.22.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.0\",\"target\":\"cfg(windows)\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"combine\",\"req\":\"^4.1.0\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"java-locator\",\"optional\":true,\"req\":\"^0.1.3\",\"target\":\"cfg(not(target_os = \\\"android\\\"))\"},{\"kind\":\"dev\",\"name\":\"javac\",\"req\":\"^0.1.0\"},{\"name\":\"jni-macros\",\"req\":\"=0.22.4\"},{\"name\":\"jni-sys\",\"req\":\"^0.4.1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_os = \\\"android\\\"))\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"rusty-fork\",\"req\":\"^0.3.0\"},{\"name\":\"simd_cesu8\",\"req\":\"^1.1.1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"walkdir\",\"req\":\"^2\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_System_Threading\",\"Win32_Foundation\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"_cfg_test\":[],\"default\":[],\"invocation\":[\"dep:java-locator\",\"dep:libloading\"]}}", "jobserver_0.1.34": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"getrandom\",\"req\":\"^0.3.2\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2.171\",\"target\":\"cfg(unix)\"},{\"features\":[\"fs\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.28.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"}],\"features\":{}}", "js-sys_0.3.85": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"wasm-bindgen/std\"]}}", "jsonptr_0.7.1": "{\"dependencies\":[{\"features\":[\"fancy\"],\"name\":\"miette\",\"optional\":true,\"req\":\"^7.4.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.203\"},{\"features\":[\"alloc\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.119\"},{\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.109\",\"target\":\"cfg(any())\"},{\"name\":\"toml\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"assign\":[],\"default\":[\"std\",\"serde\",\"json\",\"resolve\",\"assign\",\"delete\"],\"delete\":[\"resolve\"],\"json\":[\"dep:serde_json\",\"serde\"],\"miette\":[\"dep:miette\",\"std\"],\"resolve\":[],\"std\":[\"serde/std\",\"serde_json?/std\"],\"toml\":[\"dep:toml\",\"serde\",\"std\"]}}", @@ -1173,7 +1181,7 @@ "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.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"call\":[],\"default\":[\"call\",\"std\",\"redox_syscall\"],\"mkns\":[\"ioslice\"],\"std\":[]}}", "libredox_0.1.14": "{\"dependencies\":[{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"plain\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"base\":[\"libc\"],\"call\":[\"base\"],\"default\":[\"base\",\"call\",\"std\",\"redox_syscall\",\"protocol\"],\"mkns\":[\"ioslice\"],\"protocol\":[\"plain\",\"bitflags\",\"redox_syscall\"],\"std\":[\"base\"]}}", - "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\":[]}}", + "libsqlite3-sys_0.37.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.2.27\"},{\"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.89\"},{\"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\":[],\"column_metadata\":[],\"default\":[\"min_sqlite_version_3_34_1\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_34_1\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}", "libssh2-sys_0.3.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.25\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"libc\"],\"name\":\"libz-sys\",\"req\":\"^1.1.0\"},{\"name\":\"openssl-sys\",\"req\":\"^0.9.35\",\"target\":\"cfg(unix)\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.35\",\"target\":\"cfg(windows)\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.11\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"}],\"features\":{\"openssl-on-win32\":[\"openssl-sys\"],\"vendored-openssl\":[\"openssl-sys/vendored\"],\"zlib-ng-compat\":[\"libz-sys/zlib-ng\"]}}", "libz-sys_1.1.23": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}", "libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}", @@ -1203,7 +1211,7 @@ "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\":[]}}", "maybe-async_0.2.10": "{\"dependencies\":[{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit-mut\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{\"default\":[],\"is_sync\":[]}}", - "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\"]}}", + "md-5_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "md5_0.8.0": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "memchr_2.7.6": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", "memchr_2.8.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", @@ -1235,7 +1243,6 @@ "notify-types_2.1.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.34.0\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26.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\":{\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"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.3": "{\"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.59, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"derive_serde_style\":[\"serde\"],\"gnu_legacy\":[],\"std\":[]}}", - "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.2.0": "{\"dependencies\":[],\"features\":{}}", @@ -1324,7 +1331,6 @@ "pin-project_1.1.11": "{\"dependencies\":[{\"name\":\"pin-project-internal\",\"req\":\"=1.1.11\"},{\"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\":{}}", "plain_0.2.3": "{\"dependencies\":[],\"features\":{}}", @@ -1400,10 +1406,12 @@ "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.10.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rng\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.10\"},{\"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\":[],\"chacha\":[\"dep:chacha20\"],\"default\":[\"std\",\"std_rng\",\"sys_rng\",\"thread_rng\"],\"log\":[],\"serde\":[\"dep:serde\"],\"simd_support\":[],\"std\":[\"alloc\",\"getrandom?/std\"],\"std_rng\":[\"dep:chacha20\"],\"sys_rng\":[\"dep:getrandom\",\"getrandom/sys_rng\"],\"thread_rng\":[\"std\",\"std_rng\",\"sys_rng\"],\"unbiased\":[]}}", "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.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"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\":[],\"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\"]}}", "rand_chacha_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.14\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"os_rng\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\"],\"std\":[\"ppv-lite86/std\",\"rand_core/std\"]}}", + "rand_core_0.10.1": "{\"dependencies\":[],\"features\":{}}", "rand_core_0.6.4": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"serde1\":[\"serde\"],\"std\":[\"alloc\",\"getrandom\",\"getrandom/std\"]}}", "rand_core_0.9.5": "{\"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\"]}}", @@ -1426,13 +1434,13 @@ "regex-syntax_0.8.8": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\",\"unicode\"],\"std\":[],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", "regex_1.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"syntax\",\"meta\",\"nfa-pikevm\"],\"name\":\"regex-automata\",\"req\":\"^0.4.12\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"perf\",\"unicode\",\"regex-syntax/default\"],\"logging\":[\"aho-corasick?/logging\",\"memchr?/logging\",\"regex-automata/logging\"],\"pattern\":[],\"perf\":[\"perf-cache\",\"perf-dfa\",\"perf-onepass\",\"perf-backtrack\",\"perf-inline\",\"perf-literal\"],\"perf-backtrack\":[\"regex-automata/nfa-backtrack\"],\"perf-cache\":[],\"perf-dfa\":[\"regex-automata/hybrid\"],\"perf-dfa-full\":[\"regex-automata/dfa-build\",\"regex-automata/dfa-search\"],\"perf-inline\":[\"regex-automata/perf-inline\"],\"perf-literal\":[\"dep:aho-corasick\",\"dep:memchr\",\"regex-automata/perf-literal\"],\"perf-onepass\":[\"regex-automata/dfa-onepass\"],\"std\":[\"aho-corasick?/std\",\"memchr?/std\",\"regex-automata/std\",\"regex-syntax/std\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"regex-automata/unicode\",\"regex-syntax/unicode\"],\"unicode-age\":[\"regex-automata/unicode-age\",\"regex-syntax/unicode-age\"],\"unicode-bool\":[\"regex-automata/unicode-bool\",\"regex-syntax/unicode-bool\"],\"unicode-case\":[\"regex-automata/unicode-case\",\"regex-syntax/unicode-case\"],\"unicode-gencat\":[\"regex-automata/unicode-gencat\",\"regex-syntax/unicode-gencat\"],\"unicode-perl\":[\"regex-automata/unicode-perl\",\"regex-automata/unicode-word-boundary\",\"regex-syntax/unicode-perl\"],\"unicode-script\":[\"regex-automata/unicode-script\",\"regex-syntax/unicode-script\"],\"unicode-segment\":[\"regex-automata/unicode-segment\",\"regex-syntax/unicode-segment\"],\"unstable\":[\"pattern\"],\"use_std\":[\"std\"]}}", "reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}", + "reqwest_0.13.4": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"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(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.16\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\">=0.6.0, <0.8.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"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(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"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(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"}],\"features\":{\"__native-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"__native-tls-alpn\":[\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-aws-lc-rs\":[\"hyper-rustls?/aws-lc-rs\",\"tokio-rustls?/aws-lc-rs\",\"rustls?/aws-lc-rs\",\"quinn?/rustls-aws-lc-rs\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"rustls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"form\":[\"dep:serde\",\"dep:serde_urlencoded\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"dep:h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde\",\"dep:serde_json\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"__native-tls\",\"__native-tls-alpn\"],\"native-tls-no-alpn\":[\"__native-tls\"],\"native-tls-vendored\":[\"__native-tls\",\"native-tls-crate?/vendored\",\"__native-tls-alpn\"],\"native-tls-vendored-no-alpn\":[\"__native-tls\",\"native-tls-crate?/vendored\"],\"query\":[\"dep:serde\",\"dep:serde_urlencoded\"],\"rustls\":[\"__rustls-aws-lc-rs\",\"dep:rustls-platform-verifier\",\"__rustls\"],\"rustls-no-provider\":[\"dep:rustls-platform-verifier\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"zstd\":[\"tower-http/decompression-zstd\"]}}", "resb_0.1.2": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"nom\",\"optional\":true,\"req\":\"^7.0.0\"},{\"default_features\":false,\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.220\"}],\"features\":{\"default\":[],\"logging\":[\"dep:log\"],\"serialize\":[\"std\"],\"std\":[],\"text\":[\"dep:indexmap\",\"dep:nom\",\"std\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", "rfc6979_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", - "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", - "rmcp_0.15.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}", - "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\"]}}", + "rmcp-macros_1.7.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\":{\"local\":[]}}", + "rmcp_1.7.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"axum\",\"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\\\"))\"},{\"default_features\":false,\"features\":[\"serde\",\"now\"],\"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\"},{\"features\":[\"client\",\"http1\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"server\",\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"jsonwebtoken\",\"optional\":true,\"req\":\"^10\"},{\"default_features\":false,\"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.10\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.13.2\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^1.7.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\"},{\"kind\":\"dev\",\"name\":\"tower-service\",\"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\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"which\",\"optional\":true,\"req\":\"^8\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"auth-client-credentials-jwt\":[\"auth\",\"dep:jsonwebtoken\",\"uuid\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"local\":[\"rmcp-macros?/local\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"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-client-unix-socket\":[\"transport-streamable-http-client\",\"dep:hyper\",\"dep:hyper-util\",\"dep:http-body-util\",\"dep:http\",\"dep:bytes\",\"tokio/net\"],\"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\"],\"which-command\":[\"transport-child-process\",\"dep:which\"]}}", "rtrb_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "rust-embed-impl_8.11.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"rust-embed-utils\",\"req\":\"^8.11.0\"},{\"name\":\"shellexpand\",\"optional\":true,\"req\":\"^3\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"proc-macro\",\"printing\"],\"name\":\"syn\",\"req\":\"^2\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"compression\":[],\"debug-embed\":[],\"deterministic-timestamps\":[],\"include-exclude\":[\"rust-embed-utils/include-exclude\"],\"interpolate-folder-path\":[\"shellexpand\"],\"mime-guess\":[\"rust-embed-utils/mime-guess\"]}}", "rust-embed-utils_8.11.0": "{\"dependencies\":[{\"name\":\"globset\",\"optional\":true,\"req\":\"^0.4.8\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.4\"},{\"name\":\"sha2\",\"req\":\"^0.10.5\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"debug-embed\":[],\"include-exclude\":[\"globset\"],\"mime-guess\":[\"mime_guess\"]}}", @@ -1449,6 +1457,8 @@ "rustix_1.1.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", + "rustls-platform-verifier-android_0.1.1": "{\"dependencies\":[],\"features\":{}}", + "rustls-platform-verifier_0.7.0": "{\"dependencies\":[{\"name\":\"android_logger\",\"optional\":true,\"req\":\"^0.15\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"jni\",\"req\":\"^0.22\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"default_features\":false,\"name\":\"jni\",\"optional\":true,\"req\":\"^0.22.4\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.9\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.9\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"default_features\":false,\"features\":[\"ring\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"req\":\"^0.8\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"rustls-platform-verifier-android\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"security-framework\",\"req\":\"^3.5.0\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"security-framework-sys\",\"req\":\"^2.15\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"webpki-root-certs\",\"req\":\"^1\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"webpki-root-certs\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_Security_Cryptography\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.62.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"cert-logging\":[\"base64\"],\"dbg\":[],\"docsrs\":[\"jni\",\"once_cell\"],\"ffi-testing\":[\"android_logger\",\"rustls/ring\"]}}", "rustls-webpki_0.103.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18.1\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", "rustversion_1.0.22": "{\"dependencies\":[{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", @@ -1509,8 +1519,10 @@ "serial_test_derive_3.3.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\">=0.6.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"printing\",\"parsing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"async\":[],\"default\":[],\"file_locks\":[],\"test_logging\":[]}}", "sha1-checked_0.10.0": "{\"dependencies\":[{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"compress\"],\"name\":\"sha1\",\"req\":\"^0.10.6\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7\"}],\"features\":{\"default\":[\"oid\",\"std\"],\"oid\":[\"digest/oid\",\"sha1/oid\"],\"std\":[\"digest/std\",\"sha1/std\"],\"zeroize\":[\"dep:zeroize\"]}}", "sha1_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha1-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"sha1-asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", + "sha1_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "sha1_smol_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "sha2_0.10.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha2-asm\",\"optional\":true,\"req\":\"^0.6.1\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"}],\"features\":{\"asm\":[\"sha2-asm\"],\"asm-aarch64\":[\"asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"force-soft-compact\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", + "sha2_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "sharded-slab_0.1.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"^1\"},{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"features\":[\"checkpoint\"],\"name\":\"loom\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"features\":[\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"memory-stats\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"slab\",\"req\":\"^0.4.2\"}],\"features\":{}}", "shared_library_0.1.9": "{\"dependencies\":[{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\"}],\"features\":{}}", "shell-words_1.1.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -1520,6 +1532,7 @@ "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.8": "{\"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\":[]}}", + "simd_cesu8_1.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cesu8\",\"req\":\"^1.1.0\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"simdutf8\",\"req\":\"^0.1.4\"}],\"features\":{\"bench\":[],\"default\":[\"std\"],\"nightly\":[],\"std\":[\"simdutf8/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\"]}}", "simple_asn1_0.6.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\",\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.47\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\",\"parsing\",\"quickcheck\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"}],\"features\":{}}", @@ -1533,13 +1546,13 @@ "socket2_0.6.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.172\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\">=0.60, <0.62\",\"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\"]}}", + "sqlx-core_0.9.0": "{\"dependencies\":[{\"name\":\"async-fs\",\"optional\":true,\"req\":\"^2.1\"},{\"default_features\":false,\"features\":[\"async-io\"],\"name\":\"async-global-executor\",\"optional\":true,\"req\":\"^3.1\"},{\"name\":\"async-io\",\"optional\":true,\"req\":\"^2.4.1\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.13\"},{\"name\":\"async-task\",\"optional\":true,\"req\":\"^4.7.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"bytes\",\"req\":\"^1.2.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"std\",\"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.32\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"name\":\"futures-io\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"name\":\"hashbrown\",\"req\":\"^0.16.0\"},{\"name\":\"hashlink\",\"req\":\"^0.11.0\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.21.1\"},{\"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.5.0\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.10\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.24\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.142\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"default_features\":false,\"name\":\"smol\",\"optional\":true,\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.25.0\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"fs\"],\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"toml\",\"optional\":true,\"req\":\"^0.8.16\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"_rt-async-global-executor\":[\"async-global-executor\",\"_rt-async-io\",\"_rt-async-task\"],\"_rt-async-io\":[\"async-io\",\"async-fs\"],\"_rt-async-std\":[\"async-std\",\"_rt-async-io\"],\"_rt-async-task\":[\"async-task\"],\"_rt-smol\":[\"smol\",\"_rt-async-io\",\"_rt-async-task\"],\"_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\"],\"_unstable-doc\":[\"sqlx-toml\"],\"any\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"],\"migrate\":[\"sha2\",\"crc\"],\"offline\":[\"serde\",\"either/serde\"],\"sqlx-toml\":[\"serde\",\"toml/parse\"]}}", + "sqlx-macros-core_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"async-io\"],\"name\":\"async-global-executor\",\"optional\":true,\"req\":\"^3.1\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.13\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"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\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.83\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"name\":\"serde_json\",\"req\":\"^1.0.142\"},{\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"smol\",\"optional\":true,\"req\":\"^2.0\"},{\"features\":[\"offline\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.9.0\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.9.0\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"full\",\"derive\",\"parsing\",\"printing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2.0.87\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"optional\":true,\"req\":\"^2.0.18\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.25.0\"},{\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-global-executor\":[\"async-global-executor\",\"sqlx-core/_rt-async-global-executor\"],\"_rt-async-std\":[\"async-std\",\"sqlx-core/_rt-async-std\"],\"_rt-smol\":[\"smol\",\"sqlx-core/_rt-smol\"],\"_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\":[\"thiserror\"],\"migrate\":[\"sqlx-core/migrate\"],\"mysql\":[\"sqlx-mysql\"],\"mysql-rsa\":[\"mysql\",\"sqlx-mysql/rsa\"],\"postgres\":[\"sqlx-postgres\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\"],\"sqlite-load-extension\":[\"sqlx-sqlite/load-extension\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\"],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\",\"sqlx-sqlite?/sqlx-toml\"],\"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.9.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.83\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"features\":[\"any\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"sqlx-macros-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.87\"}],\"features\":{\"_rt-async-global-executor\":[\"sqlx-macros-core/_rt-async-global-executor\"],\"_rt-async-std\":[\"sqlx-macros-core/_rt-async-std\"],\"_rt-smol\":[\"sqlx-macros-core/_rt-smol\"],\"_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\"],\"mysql-rsa\":[\"sqlx-macros-core/mysql-rsa\"],\"postgres\":[\"sqlx-macros-core/postgres\"],\"rust_decimal\":[\"sqlx-macros-core/rust_decimal\"],\"sqlite\":[\"sqlx-macros-core/sqlite\"],\"sqlite-load-extension\":[\"sqlx-macros-core/sqlite-load-extension\"],\"sqlite-unbundled\":[\"sqlx-macros-core/sqlite-unbundled\"],\"sqlx-toml\":[\"sqlx-macros-core/sqlx-toml\"],\"time\":[\"sqlx-macros-core/time\"],\"uuid\":[\"sqlx-macros-core/uuid\"]}}", + "sqlx-mysql_0.9.0": "{\"dependencies\":[{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bytes\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14.4\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"rsa\",\"optional\":true,\"req\":\"^0.10.0-rc.18\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"default_features\":false,\"name\":\"sha1\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.11.0\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"sqlx-core/bigdecimal\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"default\":[],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\",\"dep:crc\"],\"offline\":[\"sqlx-core/offline\",\"serde/derive\",\"bitflags/serde\"],\"rsa\":[\"dep:rand\",\"dep:rsa\"],\"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.9.0": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"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\",\"optional\":true,\"req\":\"^3.0.0\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"etcetera\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.13.0\"},{\"default_features\":false,\"name\":\"hmac\",\"req\":\"^0.13.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.21.1\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.5.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.3\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.142\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.11.0\"},{\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^2.0.2\"}],\"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\":[\"dep:serde\",\"dep:serde_json\",\"sqlx-core/json\"],\"mac_address\":[\"dep:mac_address\",\"sqlx-core/mac_address\"],\"migrate\":[\"sqlx-core/migrate\",\"dep:crc\"],\"offline\":[\"json\",\"sqlx-core/offline\",\"smallvec/serde\"],\"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.9.0": "{\"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.12.0\"},{\"name\":\"form_urlencoded\",\"req\":\"^1.2.2\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"name\":\"futures-executor\",\"req\":\"^0.3.32\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"pkg-config\",\"vcpkg\"],\"name\":\"libsqlite3-sys\",\"req\":\">=0.30.1, <0.38.0\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"}],\"features\":{\"_unstable-all-sqlite-features\":[\"deserialize\",\"load-extension\",\"preupdate-hook\",\"unlock-notify\"],\"_unstable-all-types\":[\"json\",\"chrono\",\"time\",\"uuid\"],\"_unstable-docs\":[\"bundled\",\"any\",\"_unstable-all-types\",\"_unstable-all-sqlite-features\"],\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[],\"bundled\":[\"libsqlite3-sys/bundled\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"deserialize\":[],\"json\":[\"sqlx-core/json\",\"serde\"],\"load-extension\":[],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde\"],\"preupdate-hook\":[\"libsqlite3-sys/preupdate_hook\"],\"regexp\":[\"dep:regex\"],\"rust_decimal\":[],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"unbundled\":[\"libsqlite3-sys/buildtime_bindgen\"],\"unlock-notify\":[\"libsqlite3-sys/unlock_notify\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13\"},{\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.37.0\"},{\"features\":[\"bundled-sqlcipher\"],\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.37.0\",\"target\":\"cfg(sqlite_test_sqlcipher)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.6\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.142\"},{\"features\":[\"migrate\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"sqlx-macros\",\"optional\":true,\"req\":\"=0.9.0\"},{\"default_features\":false,\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.9.0\"},{\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.9.0\"},{\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.9.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"kind\":\"dev\",\"name\":\"time_\",\"package\":\"time\",\"req\":\"^0.3.47\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.53\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-global-executor\":[],\"_rt-async-std\":[],\"_rt-smol\":[],\"_rt-tokio\":[],\"_sqlite\":[],\"_unstable-all-types\":[\"bigdecimal\",\"rust_decimal\",\"json\",\"time\",\"chrono\",\"ipnet\",\"ipnetwork\",\"mac_address\",\"uuid\",\"bit-vec\",\"bstr\"],\"_unstable-docs\":[\"all-databases\",\"_unstable-all-types\",\"sqlx-sqlite/_unstable-docs\"],\"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\",\"sqlx-core/offline\",\"sqlx-mysql?/offline\",\"sqlx-postgres?/offline\",\"sqlx-sqlite?/offline\"],\"migrate\":[\"sqlx-core/migrate\",\"sqlx-macros?/migrate\",\"sqlx-mysql?/migrate\",\"sqlx-postgres?/migrate\",\"sqlx-sqlite?/migrate\"],\"mysql\":[\"sqlx-mysql\",\"sqlx-macros?/mysql\"],\"mysql-rsa\":[\"mysql\",\"sqlx-mysql/rsa\",\"sqlx-macros?/mysql-rsa\"],\"postgres\":[\"sqlx-postgres\",\"sqlx-macros?/postgres\"],\"regexp\":[\"sqlx-sqlite?/regexp\"],\"runtime-async-global-executor\":[\"_rt-async-global-executor\",\"sqlx-core/_rt-async-global-executor\",\"sqlx-macros?/_rt-async-global-executor\"],\"runtime-async-std\":[\"_rt-async-std\",\"sqlx-core/_rt-async-std\",\"sqlx-macros?/_rt-async-std\"],\"runtime-smol\":[\"_rt-smol\",\"sqlx-core/_rt-smol\",\"sqlx-macros?/_rt-smol\"],\"runtime-tokio\":[\"_rt-tokio\",\"sqlx-core/_rt-tokio\",\"sqlx-macros?/_rt-tokio\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-macros?/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"sqlite-bundled\",\"sqlite-deserialize\",\"sqlite-load-extension\",\"sqlite-unlock-notify\"],\"sqlite-bundled\":[\"_sqlite\",\"sqlx-sqlite/bundled\",\"sqlx-macros?/sqlite\"],\"sqlite-deserialize\":[\"sqlx-sqlite/deserialize\"],\"sqlite-load-extension\":[\"sqlx-sqlite/load-extension\",\"sqlx-macros?/sqlite-load-extension\"],\"sqlite-preupdate-hook\":[\"sqlx-sqlite/preupdate-hook\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\",\"sqlx-macros?/sqlite-unbundled\"],\"sqlite-unlock-notify\":[\"sqlx-sqlite/unlock-notify\"],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\",\"sqlx-macros?/sqlx-toml\",\"sqlx-sqlite?/sqlx-toml\"],\"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.1": "{\"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\":{}}", @@ -1659,7 +1672,7 @@ "ts-rs_11.1.0": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"bigdecimal\",\"optional\":true,\"req\":\">=0.0.13, <0.5\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"dprint-plugin-typescript\",\"optional\":true,\"req\":\"=0.95\"},{\"name\":\"heapless\",\"optional\":true,\"req\":\">=0.7, <0.9\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ordered-float\",\"optional\":true,\"req\":\">=3, <6\"},{\"name\":\"semver\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smol_str\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"sync\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.40\"},{\"name\":\"ts-rs-macros\",\"req\":\"=11.1.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"bigdecimal-impl\":[\"bigdecimal\"],\"bson-uuid-impl\":[\"bson\"],\"bytes-impl\":[\"bytes\"],\"chrono-impl\":[\"chrono\"],\"default\":[\"serde-compat\"],\"format\":[\"dprint-plugin-typescript\"],\"heapless-impl\":[\"heapless\"],\"import-esm\":[],\"indexmap-impl\":[\"indexmap\"],\"no-serde-warnings\":[\"ts-rs-macros/no-serde-warnings\"],\"ordered-float-impl\":[\"ordered-float\"],\"semver-impl\":[\"semver\"],\"serde-compat\":[\"ts-rs-macros/serde-compat\"],\"serde-json-impl\":[\"serde_json\"],\"smol_str-impl\":[\"smol_str\"],\"tokio-impl\":[\"tokio\"],\"url-impl\":[\"url\"],\"uuid-impl\":[\"uuid\"]}}", "two-face_0.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cargo-lock\",\"req\":\"^10.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.44.3\"},{\"default_features\":false,\"features\":[\"read\"],\"kind\":\"dev\",\"name\":\"object\",\"req\":\"^0.36.7\"},{\"name\":\"serde\",\"req\":\"^1.0.228\"},{\"name\":\"serde_derive\",\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26.3\"},{\"default_features\":false,\"features\":[\"dump-load\",\"parsing\"],\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"default_features\":false,\"features\":[\"html\"],\"kind\":\"dev\",\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.23\"},{\"default_features\":false,\"features\":[\"std\",\"xxhash64\"],\"kind\":\"dev\",\"name\":\"twox-hash\",\"req\":\"^2.1.2\"}],\"features\":{\"default\":[\"syntect-onig\"],\"syntect-default-fancy\":[\"syntect-fancy\",\"syntect/default-fancy\"],\"syntect-default-onig\":[\"syntect-onig\",\"syntect/default-onig\"],\"syntect-fancy\":[\"syntect/regex-fancy\"],\"syntect-onig\":[\"syntect/regex-onig\"]}}", "type-map_0.5.1": "{\"dependencies\":[{\"name\":\"rustc-hash\",\"req\":\"^2\"}],\"features\":{}}", - "typenum_1.19.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"force_unix_path_separator\":[],\"i128\":[],\"no_std\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}", + "typenum_1.20.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"i128\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}", "uds_windows_1.1.0": "{\"dependencies\":[{\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"tempfile\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"features\":[\"winsock2\",\"ws2def\",\"minwinbase\",\"ntdef\",\"processthreadsapi\",\"handleapi\",\"ws2tcpip\",\"winbase\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "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\":{}}", @@ -1714,6 +1727,7 @@ "wasm-encoder_0.244.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"default_features\":false,\"name\":\"leb128fmt\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2.0\"},{\"default_features\":false,\"features\":[\"simd\",\"simd\"],\"name\":\"wasmparser\",\"optional\":true,\"req\":\"^0.244.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"wasmprinter\",\"req\":\"^0.244.0\"}],\"features\":{\"component-model\":[\"wasmparser?/component-model\"],\"default\":[\"std\",\"component-model\"],\"std\":[\"wasmparser?/std\"]}}", "wasm-metadata_0.244.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"name\":\"auditable-serde\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\"],\"name\":\"clap\",\"optional\":true,\"req\":\"^4.0.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"indexmap\",\"req\":\"^2.7.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0.166\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"spdx\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"features\":[\"std\",\"component-model\"],\"name\":\"wasm-encoder\",\"req\":\"^0.244.0\"},{\"default_features\":false,\"features\":[\"simd\",\"std\",\"component-model\",\"hash-collections\"],\"name\":\"wasmparser\",\"req\":\"^0.244.0\"}],\"features\":{\"default\":[\"oci\",\"serde\"],\"oci\":[\"dep:auditable-serde\",\"dep:flate2\",\"dep:url\",\"dep:spdx\",\"dep:serde_json\",\"serde\"],\"serde\":[\"dep:serde_derive\",\"dep:serde\"]}}", "wasm-streams_0.4.2": "{\"dependencies\":[{\"features\":[\"io\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"features\":[\"futures\"],\"kind\":\"dev\",\"name\":\"gloo-timers\",\"req\":\"^0.3.0\"},{\"name\":\"js-sys\",\"req\":\"^0.3.72\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.95\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.45\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"features\":[\"AbortSignal\",\"QueuingStrategy\",\"ReadableStream\",\"ReadableStreamType\",\"ReadableWritablePair\",\"ReadableStreamByobReader\",\"ReadableStreamReaderMode\",\"ReadableStreamReadResult\",\"ReadableStreamByobRequest\",\"ReadableStreamDefaultReader\",\"ReadableByteStreamController\",\"ReadableStreamGetReaderOptions\",\"ReadableStreamDefaultController\",\"StreamPipeOptions\",\"TransformStream\",\"TransformStreamDefaultController\",\"Transformer\",\"UnderlyingSink\",\"UnderlyingSource\",\"WritableStream\",\"WritableStreamDefaultController\",\"WritableStreamDefaultWriter\"],\"name\":\"web-sys\",\"req\":\"^0.3.72\"},{\"features\":[\"console\",\"AbortSignal\",\"ErrorEvent\",\"PromiseRejectionEvent\",\"Response\",\"ReadableStream\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3.72\"}],\"features\":{}}", + "wasm-streams_0.5.0": "{\"dependencies\":[{\"features\":[\"io\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"features\":[\"futures\"],\"kind\":\"dev\",\"name\":\"gloo-timers\",\"req\":\"^0.3.0\"},{\"name\":\"js-sys\",\"req\":\"^0.3.85\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.108\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.58\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.58\"},{\"features\":[\"AbortSignal\",\"QueuingStrategy\",\"ReadableStream\",\"ReadableStreamType\",\"ReadableWritablePair\",\"ReadableStreamByobReader\",\"ReadableStreamReaderMode\",\"ReadableStreamReadResult\",\"ReadableStreamByobRequest\",\"ReadableStreamDefaultReader\",\"ReadableByteStreamController\",\"ReadableStreamGetReaderOptions\",\"ReadableStreamDefaultController\",\"StreamPipeOptions\",\"TransformStream\",\"TransformStreamDefaultController\",\"Transformer\",\"UnderlyingSink\",\"UnderlyingSource\",\"WritableStream\",\"WritableStreamDefaultController\",\"WritableStreamDefaultWriter\"],\"name\":\"web-sys\",\"req\":\"^0.3.85\"},{\"features\":[\"console\",\"AbortSignal\",\"ErrorEvent\",\"PromiseRejectionEvent\",\"Response\",\"ReadableStream\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3.85\"}],\"features\":{}}", "wasmparser_0.244.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"name\":\"bitflags\",\"req\":\"^2.4.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.2\"},{\"default_features\":false,\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.7.0\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.17\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.13.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"semver\",\"optional\":true,\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.166\"}],\"features\":{\"component-model\":[\"dep:semver\"],\"default\":[\"std\",\"validate\",\"serde\",\"features\",\"component-model\",\"hash-collections\",\"simd\"],\"features\":[],\"hash-collections\":[\"dep:hashbrown\",\"dep:indexmap\"],\"prefer-btree-collections\":[],\"serde\":[\"dep:serde\",\"indexmap?/serde\",\"hashbrown?/serde\"],\"simd\":[],\"std\":[\"indexmap?/std\"],\"validate\":[]}}", "wayland-backend_0.3.12": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"concat-idents\",\"req\":\"^1.1\"},{\"name\":\"downcast-rs\",\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"raw-window-handle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"features\":[\"event\",\"fs\",\"net\",\"process\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"name\":\"rwh_06\",\"optional\":true,\"package\":\"raw-window-handle\",\"req\":\"^0.6.0\"},{\"name\":\"scoped-tls\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"scoped-tls\",\"req\":\"^1.0\"},{\"features\":[\"union\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.9\"},{\"name\":\"wayland-sys\",\"req\":\"^0.31.8\"}],\"features\":{\"client_system\":[\"wayland-sys/client\",\"dep:scoped-tls\"],\"dlopen\":[\"wayland-sys/dlopen\"],\"server_system\":[\"wayland-sys/server\",\"dep:scoped-tls\"]}}", "wayland-client_0.31.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"}],\"features\":{}}", @@ -1725,12 +1739,12 @@ "web-time_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.20\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"macro\"],\"kind\":\"dev\",\"name\":\"pollster\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"^0.2.70\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"features\":[\"WorkerGlobalScope\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"CssStyleDeclaration\",\"Document\",\"Element\",\"HtmlTableElement\",\"HtmlTableRowElement\",\"Performance\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", "webbrowser_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.0\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\">=0.3, <=0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"NSDictionary\",\"NSString\",\"NSURL\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^0.10\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"Window\"],\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"disable-wsl\":[],\"hardened\":[],\"wasm-console\":[\"web-sys/console\"]}}", "webpki-root-certs_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"}],\"features\":{}}", - "webpki-roots_0.26.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"parent\",\"package\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "webpki-roots_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "weezl_0.1.12": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.12\"},{\"default_features\":false,\"features\":[\"macros\",\"io-util\",\"net\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"compat\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.2\"}],\"features\":{\"alloc\":[],\"async\":[\"futures\",\"std\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "which_6.0.3": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.9.0\"},{\"name\":\"home\",\"req\":\"^0.5.9\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^0.38.30\",\"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\",\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "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\"]}}", + "whoami_2.1.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.186\",\"target\":\"cfg(all(any(target_vendor = \\\"apple\\\", target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"illumos\\\", target_os = \\\"hurd\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"features\":[\"call\"],\"name\":\"libredox\",\"optional\":true,\"req\":\"^0.1.16\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"features\":[\"SCDynamicStore\",\"SCDynamicStoreCopySpecific\"],\"name\":\"objc2-system-configuration\",\"optional\":true,\"req\":\"^0.3.2\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"default_features\":false,\"name\":\"wasite\",\"optional\":true,\"req\":\"^1.0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3.85\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"std\",\"wasi-wasite\",\"wasm-web\"],\"force-stub\":[],\"std\":[\"dep:libc\",\"dep:libredox\",\"dep:objc2-system-configuration\",\"libc?/std\",\"web-sys?/std\"],\"wasi-wasite\":[\"dep:wasite\"],\"wasm-web\":[\"dep: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\"]}}", @@ -1867,112 +1881,112 @@ "zvariant_utils_2.1.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.81\"},{\"name\":\"quote\",\"req\":\"^1.0.36\"},{\"features\":[\"extra-traits\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.64\"}],\"features\":{}}" }, "@@rules_rs+//rs/experimental/toolchains:module_extension.bzl%toolchains": { - "cargo-1.93.0-aarch64-apple-darwin.tar.xz": "6443909350322ad07f09bb5edfd9ff29268e6fe88c7d78bfba7a5e254248dc25", - "cargo-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "387832b989c8eb96c9ebd66402a87962167633bc6d91a49ffb8c7903c45f3476", - "cargo-1.93.0-aarch64-pc-windows-msvc.tar.xz": "155bff7a16aa7054e7ed7c3a82e362d4b302b3882d751b823e06ff63ae3f103d", - "cargo-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "5998940b8b97286bb67facb1a85535eeb3d4d7a61e36a85e386e5c0c5cfe5266", - "cargo-1.93.0-x86_64-apple-darwin.tar.xz": "95a47c5ed797c35419908f04188d8b7de09946e71073c4b72632b16f5b10dfae", - "cargo-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "f19766837559f90476508140cb95cc708220012ec00a854fa9f99187b1f246b6", - "cargo-1.93.0-x86_64-pc-windows-msvc.tar.xz": "e59c5e2baa9ec17261f2cda6676ebf7b68b21a860e3f7451c4d964728951da75", - "cargo-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "c23de3ae709ff33eed5e4ae59d1f9bcd75fa4dbaa9fb92f7b06bfb534b8db880", - "clippy-1.93.0-aarch64-apple-darwin.tar.xz": "0b6e943a8d12be0e68575acf59c9ea102daf795055fcbbf862b0bfd35ec40039", - "clippy-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "296949f49be2de77ce9d1c5023a5f0a58e28e329eec03642a3da0e175a67beeb", - "clippy-1.93.0-aarch64-pc-windows-msvc.tar.xz": "07bcf2edb88cdf5ead2f02e4a8493e9b0ef935a31253fac6f9f3378d8023f113", - "clippy-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "872ae6d68d625946d281b91d928332e6b74f6ab269b6af842338df4338805a60", - "clippy-1.93.0-x86_64-apple-darwin.tar.xz": "e6d0b1afb9607c14a1172d09ee194a032bbb3e48af913d55c5a473e0559eddde", - "clippy-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "b6f1f7264ed6943c59dedfb9531fbadcc3c0fcf273c940a63d58898b14a1060f", - "clippy-1.93.0-x86_64-pc-windows-msvc.tar.xz": "25fb103390bf392980b4689ac09b2ec2ab4beefb7022a983215b613ad05eab57", - "clippy-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "793108977514b15c0f45ade28ae35c58b05370cb0f22e89bd98fdfa61eabf55d", - "rust-analyzer-1.93.0-aarch64-apple-darwin.tar.xz": "8a09a46d45277678b2d112eef61736e03e78dfa6c506e187df176b904659e5a7", - "rust-analyzer-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "1fb48fadbaea6da36cfcd6b72122eeaa6ab025c9c82441777cf913108f73c115", - "rust-analyzer-1.93.0-aarch64-pc-windows-msvc.tar.xz": "558100ab62fe22d552be43c1bf166fef057ff2f3eb0d786cf5fe509a08701f37", - "rust-analyzer-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "b2e7890cf5953f572eb05d7328c086fa60d4f16e5d301d35c52b3f58b88a8316", - "rust-analyzer-1.93.0-x86_64-apple-darwin.tar.xz": "3cb7975d4fd17840e2980c9d864755f801ee9e7594de886a5816c73b07594508", - "rust-analyzer-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "4471db6393b7380624899482aae99612fd1cf509ae36d0c68292077c2104127d", - "rust-analyzer-1.93.0-x86_64-pc-windows-msvc.tar.xz": "f4c1f8f120f48974cb1e0cea01a955ce52cdfa0f1db0355c5fb5a2deeca8188a", - "rust-analyzer-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "33fcd377be3b5ffdd95977c3d0219f63725c18b6c8b53fb5be0418962a84738c", - "rust-src-1.93.0.tar.xz": "0e7b9acd5debfeffef3741dc8f6edf137d70426a0027d6b190cc8cfb0a1ac23c", - "rust-std-1.93.0-aarch64-apple-darwin.tar.xz": "8603c63715349636ed85b4fe716c4e827a727918c840e54aff5b243cedadf19b", - "rust-std-1.93.0-aarch64-apple-ios-macabi.tar.xz": "24d47e615ce101869ff452a572a6b77ed7cf70f2454d0b50892ac849e8c7ac4d", - "rust-std-1.93.0-aarch64-apple-ios-sim.tar.xz": "d1d5e2d1b79206f2cc9fb7f6a2958cfe0f4bbc9147fda8dbc3608aa4be5e6816", - "rust-std-1.93.0-aarch64-apple-ios.tar.xz": "49228e70387773a71cf144509baf39979ab2cdb604340fff64b483ab41f61617", - "rust-std-1.93.0-aarch64-linux-android.tar.xz": "59c16648d9a29c07f63a1749cae6b73078f20fef1206c5e0f81c784ae8796cdb", - "rust-std-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "9a270d50eaaacc7cb1925565a8b65ff831686aa1592b7034bb9848d7f2a9738d", - "rust-std-1.93.0-aarch64-pc-windows-msvc.tar.xz": "f7bd3d25baf3643c8769b8c4d2e6cde45bb25042fac698e0daf19fc9f58f8568", - "rust-std-1.93.0-aarch64-unknown-fuchsia.tar.xz": "d1e46c443a9607603c810942e99a95a1acfb105d1312426b468ff68febaabf77", - "rust-std-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "84e82ff52c39c64dfd0e1c2d58fd3d5309d1d2502378131544c0d486b44af20a", - "rust-std-1.93.0-aarch64-unknown-linux-musl.tar.xz": "bab885a87da586040064064bd1c314d707164d8dc0fefee39d59be7f15ce6f7d", - "rust-std-1.93.0-aarch64-unknown-none-softfloat.tar.xz": "0f6305daf658a7d6c0efd075859cb60432c13b82e7ecee0d097074e4e1873391", - "rust-std-1.93.0-aarch64-unknown-none.tar.xz": "3cf1aa3309a8903e89bb20537113155ca4e88844c8cc9c34c43865d5ce5a6192", - "rust-std-1.93.0-aarch64-unknown-uefi.tar.xz": "317b0af124e0e124bd76b8e5a2fb0c600279177d0bed9c841a3202df2d0f7f8e", - "rust-std-1.93.0-arm-linux-androideabi.tar.xz": "d010b26fc88e28a93cc94ea6ca5d2c90efed7f9846fae1e40db7c183b50575e2", - "rust-std-1.93.0-arm-unknown-linux-gnueabi.tar.xz": "deedc54ffce099781986eed4aec0843866f1bf72904ab0f5cdb115b9c7af540e", - "rust-std-1.93.0-arm-unknown-linux-gnueabihf.tar.xz": "89e44e042bc1241b3999191c385fec8433d60a5a9fc373583cd3b2d9408d5074", - "rust-std-1.93.0-arm-unknown-linux-musleabi.tar.xz": "641a17acb5104637d4dc9c4be022a7927ae721eb08759fea96ecfaf5c60be4dc", - "rust-std-1.93.0-arm-unknown-linux-musleabihf.tar.xz": "94a92b454bf3b0aab046b257f555ccb08f16dc2dc281bea6a4ef17ea8f58cbdc", - "rust-std-1.93.0-armv7-linux-androideabi.tar.xz": "e295f26bb219a7a4ebb5c2e07fedfebb075be6830aaf910c742a57cd21018b6d", - "rust-std-1.93.0-armv7-unknown-linux-gnueabi.tar.xz": "8a7bd5227c78294864095edb07837ff32ff6c07cd1a4a418f9bcc3ebd7e79874", - "rust-std-1.93.0-armv7-unknown-linux-gnueabihf.tar.xz": "f015f9b2d588454a9dc62942ab2e800d82c602e4eab6f89f8213419491bcd203", - "rust-std-1.93.0-armv7-unknown-linux-musleabi.tar.xz": "d15d24c9fb7c15243e1341cea53590002df271060118914bd0efcda8ccbd0731", - "rust-std-1.93.0-armv7-unknown-linux-musleabihf.tar.xz": "a2e5ec22ed35fb51a503d1e10b37447b0fa7333f079585bc0b6a2eb599de43f3", - "rust-std-1.93.0-i686-linux-android.tar.xz": "68fd86f62dd63221717d1741210f0f5cf75da7a1e32eed5a46b1e67c9d9430e1", - "rust-std-1.93.0-i686-pc-windows-gnu.tar.xz": "cb613d5d1eb245e8a1f4c0b25f93c2997cd06c1cc3fc202155f2997aebf44d4d", - "rust-std-1.93.0-i686-pc-windows-gnullvm.tar.xz": "0f713dc252a6de706519fe6cdaab6d66aaf1b555133b536cc0ab28061aa4269c", - "rust-std-1.93.0-i686-pc-windows-msvc.tar.xz": "33dc1951e2dc21bd05361160d52f496eecf48e0b95df5083172698b1cd5b9a3f", - "rust-std-1.93.0-i686-unknown-freebsd.tar.xz": "67718aae1381879fdcca5699051eb87e0cda3d2fd0fe75e306ba0948b79df7db", - "rust-std-1.93.0-i686-unknown-linux-gnu.tar.xz": "b8b7020a61418b95c1ea26badaf8db6979778e28dbadddc81fb5010fe27c935b", - "rust-std-1.93.0-i686-unknown-linux-musl.tar.xz": "867e54b3e89dc0b6d2b7a538a80443add6c3990bb4bd2260dea2ed98a0dc9870", - "rust-std-1.93.0-i686-unknown-uefi.tar.xz": "929fd484b08d5b2077ff864f5f2d24b51a78f1b6e837b9eab9d7e8fb7f31adce", - "rust-std-1.93.0-powerpc-unknown-linux-gnu.tar.xz": "e851c0fa3e726ce3f7139c5803198a1aa9723594394734ac9e373c93d92e5ea3", - "rust-std-1.93.0-powerpc64-unknown-linux-gnu.tar.xz": "f729bb7d95705e12a92eb072e494b93d8822ca40aa4802ca780b0cf33b56d401", - "rust-std-1.93.0-powerpc64le-unknown-linux-gnu.tar.xz": "d209ac698a69ca9b9035adb97a0ed8e60a08db52960198c3e03b9ee714c1a46b", - "rust-std-1.93.0-powerpc64le-unknown-linux-musl.tar.xz": "34b98d5eca2fdbd6ba41b0faf14160ef1ebd038f6ecaa264d318ad33263e1cf1", - "rust-std-1.93.0-riscv32imc-unknown-none-elf.tar.xz": "71af84c81241cbc7811b267927990be025f30d7d3dc55df4b56da7ac250f7c78", - "rust-std-1.93.0-riscv64gc-unknown-linux-gnu.tar.xz": "b769fb6c9f3e0419a6bd0b7b79f9191bbd7a48a9f243b23eb7d135711aa6de1b", - "rust-std-1.93.0-riscv64gc-unknown-linux-musl.tar.xz": "a7ced602573d814d875d69022e026c1ccb520b4b2de9d430ddfd0966ec6c9643", - "rust-std-1.93.0-riscv64gc-unknown-none-elf.tar.xz": "842f72913f288a0c76601438e67ccd88c816dbf187587928e48bf8b9ce74cbf3", - "rust-std-1.93.0-s390x-unknown-linux-gnu.tar.xz": "41a65db45a288eb3eedb187b366f132d5b3615767de7ce994b123b342ac7a848", - "rust-std-1.93.0-thumbv6m-none-eabi.tar.xz": "be3f8aad5680dabb203300847dcbbabc15729170ba5c3a9c499efae4df410a9e", - "rust-std-1.93.0-thumbv7em-none-eabi.tar.xz": "8f93eefca39c0da417feddab64775f862c72bbe80da11914dcf47babef8f1644", - "rust-std-1.93.0-thumbv7em-none-eabihf.tar.xz": "a3b6914b966ac93dbe7531016d5f570b189445603c43614a60e0b9ea12674bd3", - "rust-std-1.93.0-thumbv7m-none-eabi.tar.xz": "cfa6227214f3ae58c06b36547c5bd6f0f6787764afa48cfa4ff3488264deab6c", - "rust-std-1.93.0-thumbv8m.main-none-eabi.tar.xz": "5f24df0aa8322561125575e365be7ad13a5bb26cf73c7fc9a3f4bcfa58e0febc", - "rust-std-1.93.0-thumbv8m.main-none-eabihf.tar.xz": "906b07580be2df277cced2b56bc03cb565b758c382bf3e82cbd8375b459815dd", - "rust-std-1.93.0-wasm32-unknown-emscripten.tar.xz": "63cdbb1ea7f353060539c00f7346f4f5fb0d6f09899cacddc1f172ef07c4af8b", - "rust-std-1.93.0-wasm32-unknown-unknown.tar.xz": "3100cb920ddac646943243f0eddd331128836b9161dd5f7b0a6c76375d39cc5e", - "rust-std-1.93.0-wasm32-wasip1-threads.tar.xz": "439c65dea31e855f0258632b6d19435ba8a80561297fa6dc6be48048c5cd1871", - "rust-std-1.93.0-wasm32-wasip1.tar.xz": "075de970ef865678dad258f1566d7cfe76a594698e9bf93dd69fa5cfdfcf1a6f", - "rust-std-1.93.0-wasm32-wasip2.tar.xz": "0ef01bb552036ab44456f5505015b13c88d3694629804d7af46452c8b0a48f8c", - "rust-std-1.93.0-x86_64-apple-darwin.tar.xz": "f112d41c8a31794f0f561d37fe77010ed0b405fa70284a2910891869d8c52418", - "rust-std-1.93.0-x86_64-apple-ios-macabi.tar.xz": "a543dd545747d372d973ace8b485a13603ce96c110c7ae734d605e45f6e162c5", - "rust-std-1.93.0-x86_64-apple-ios.tar.xz": "e151013b9bc5990e178285a33e62bae7700d8c48c06e97771abb1643aa907d75", - "rust-std-1.93.0-x86_64-linux-android.tar.xz": "dc05ca79d9fecc5ce3643adb9c6f89fd35c8e1d7146bf9b62e30bad41f9fb6a7", - "rust-std-1.93.0-x86_64-pc-windows-gnu.tar.xz": "a07c6ab596fad15ca7acd63ee7f2a5fea93fd421179252067e309c2aa0b2021b", - "rust-std-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "ef6cf0977bc5aa4bbd594afb9df4ba76fdd4f0fc5685cddbefff49ceed202a91", - "rust-std-1.93.0-x86_64-pc-windows-msvc.tar.xz": "2593e29af0b8def34ceb1185b8e85bd93a9e0d3b0c108d704c1b31370c50a48c", - "rust-std-1.93.0-x86_64-unknown-freebsd.tar.xz": "51b2feaff7c2d28633504ed92ab442a55d112e6a2bf09c91188f00dbaf03378a", - "rust-std-1.93.0-x86_64-unknown-fuchsia.tar.xz": "41f0f3eb96cedfc13ab5fd4f15065063f262d035c1f71d96c42587acdacdbabe", - "rust-std-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "a849a418d0f27e69573e41763c395e924a0b98c16fcdc55599c1c79c27c1c777", - "rust-std-1.93.0-x86_64-unknown-linux-musl.tar.xz": "874658d2ced1ed2b9bf66c148b78a2e10cad475d0a4db32e68a08900905b89b8", - "rust-std-1.93.0-x86_64-unknown-netbsd.tar.xz": "aad63193af89772031f9a85f193afc0b15f8e6d4a9a4983c5f5d3802f69a89e8", - "rust-std-1.93.0-x86_64-unknown-none.tar.xz": "01dcca7ae4b7e82fbfa399adb5e160afaa13143e5a17e1e0737c38cf07365fb3", - "rust-std-1.93.0-x86_64-unknown-uefi.tar.xz": "ec4e439d9485ce752b56999e8e41ed82373fc833a005cf2531c6f7ef7e785392", - "rustc-1.93.0-aarch64-apple-darwin.tar.xz": "092be03c02b44c405dab1232541c84f32b2d9e8295747568c3d531dd137221dc", - "rustc-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "d3bc0cdaf157e20b1f23e510b5e3c4c6e9117d08f5284c04dee60aecff1bc851", - "rustc-1.93.0-aarch64-pc-windows-msvc.tar.xz": "a3ac1a8e411de8470f71b366f89d187718c431526912b181692ed0a18c56c7ad", - "rustc-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "1a9045695892ec08d8e9751bf7cf7db71fe27a6202dd12ce13aca48d0602dbde", - "rustc-1.93.0-x86_64-apple-darwin.tar.xz": "594bb293f0a4f444656cf8dec2149fcb979c606260efee9e09bcf8c9c6ed6ae7", - "rustc-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "0cdaa8de66f5ce21d1ea73917efc5c64f408bda49f678ddde19465ced9d5ec63", - "rustc-1.93.0-x86_64-pc-windows-msvc.tar.xz": "fa17677eee0d83eb055b309953184bf87ba634923d8897f860cda65d55c6e350", - "rustc-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "00c6e6740ea6a795e33568cd7514855d58408a1180cd820284a7bbf7c46af715", - "rustfmt-1.93.0-aarch64-apple-darwin.tar.xz": "0dd1faedf0768ef362f4aae4424b34e8266f2b9cf5e76ea4fcaf780220b363a0", - "rustfmt-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "5888827e7fbd7d59930870b4856fce8d6d8fca5e02f6535f8ae3d7ad0ccf2d4a", - "rustfmt-1.93.0-aarch64-pc-windows-msvc.tar.xz": "24eed108489567133bbfe40c8eacda1567be55fae4c526911b39eb33eb27a6cb", - "rustfmt-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "92e1acb45ae642136258b4dabb39302af2d53c83e56ebd5858bc969f9e5c141a", - "rustfmt-1.93.0-x86_64-apple-darwin.tar.xz": "c8453b4c5758eb39423042ffa9c23ed6128cbed2b15b581e5e1192c9cc0b1d4e", - "rustfmt-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "47167e9e78db9be4503a060dee02f4df2cda252da32175dbf44331f965a747b9", - "rustfmt-1.93.0-x86_64-pc-windows-msvc.tar.xz": "5becc7c2dba4b9ab5199012cad30829235a7f7fb5d85a238697e8f0e44cbd9af", - "rustfmt-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "7f81f6c17d11a7fda5b4e1b111942fb3b23d30dcec767e13e340ebfb762a5e33" + "cargo-1.95.0-aarch64-apple-darwin.tar.xz": "6c2ffed8e1ac9cf4dc9e80f282a869a6b237a153e7c55cca039d33de29d80aaf", + "cargo-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "14683a8e0b0ee8afdd7e2896fc8e91a7ff0ba55c2e20912e639eea498f1e1d10", + "cargo-1.95.0-aarch64-pc-windows-msvc.tar.xz": "e645b30fa035a18aa12d28b699052014c7efa9dd4a33dabd223f0d16b5fa28e8", + "cargo-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "7c070aeba9bbf12073646995a03f36c346bb5f541d0078ba6d9dc2a7adaaf6af", + "cargo-1.95.0-x86_64-apple-darwin.tar.xz": "e2e1131ade2dddc0d779e0ab3a6a990085c7a654951235742823c3a1ce0f190f", + "cargo-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "239c098b9878ad01ad5e0feeee377e6ee3311bc1534b1fb5eb630489d463bab2", + "cargo-1.95.0-x86_64-pc-windows-msvc.tar.xz": "cab2606cb2d0aa31c55d50512fe07a9f15e893227566fbeb448306760cd0d2bf", + "cargo-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "e74edd2cf7d0f1f1383b4f00eb90c843750bc489e2ccf7214e6476678a907425", + "clippy-1.95.0-aarch64-apple-darwin.tar.xz": "fd183baa023d0c4e0c5b8184226e2d4c85126adf156cb1f3a726ec593bba8d62", + "clippy-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "b81d0fe05c4ec514aefaffdf0649b175a2f82572163202c17531358f196b6168", + "clippy-1.95.0-aarch64-pc-windows-msvc.tar.xz": "44c1b7ada72aa8f3fcaceb37a3899665bc9b160c2fea77879c8ecb65a9e97eba", + "clippy-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "fb021e0c0fc2238be9266d7614f4a26bc372544c4cba3528d729ab24ad229fc9", + "clippy-1.95.0-x86_64-apple-darwin.tar.xz": "e47367f6b1489d74cbba93b387310adcb82e27a51e44b2c6ff543eb4f199fe32", + "clippy-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "9358dfc3b831a5f4b3a3a0016da734b4ef5e78c84ca8f148f56a9c126515ebfa", + "clippy-1.95.0-x86_64-pc-windows-msvc.tar.xz": "ddc151d6f58c6658b7380292ecaef36e62d063bbdbf7f5802669810575bb5b75", + "clippy-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "ac779bc9839dd47180806b133e4e2563c4a34716284cd5b8fede8ef289f452ca", + "rust-analyzer-1.95.0-aarch64-apple-darwin.tar.xz": "11231fc6574301b94bd379af4ef409caef7c65b877bcecf2b227dc0d74aa0ec7", + "rust-analyzer-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "6b2c0820957fdf0e3026f51fe85ad3cef94d5948e29cc83d2221f65dafc7a16b", + "rust-analyzer-1.95.0-aarch64-pc-windows-msvc.tar.xz": "92958624f23d4b0980748ac9e6d67f6f67a868f8224e8c6240e3f84145e2d805", + "rust-analyzer-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "b37e5b9aad624e54228254f98a710ee19ad464fe7ada93ef12e20c87886a0047", + "rust-analyzer-1.95.0-x86_64-apple-darwin.tar.xz": "6cd111900e13fd19b188c5d8844b34136af3967066c0ea2914ce5c3508296c85", + "rust-analyzer-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "01bd05a6b990ad37907ff26e8c285c5ba8b7e674fabd0e264fc7f7aa04d963c5", + "rust-analyzer-1.95.0-x86_64-pc-windows-msvc.tar.xz": "ba58e349f5e8b0ef13735c48d4ad8d8c7664472f8403f3c9d97b291bd54a7638", + "rust-analyzer-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "a9d71c6e7427c45afcd846a8b34a3e3301ae7a0e91a2bcf929326af77a7dc68e", + "rust-src-1.95.0.tar.xz": "67b09138c8db96afc4bbfc69ea771ac9a091fd777698acb43f6dfd9fb7dea363", + "rust-std-1.95.0-aarch64-apple-darwin.tar.xz": "9b30089b0f767cb91b2190ffec55a9beeb2a21a1405d8da0f664d7e09d08e6d8", + "rust-std-1.95.0-aarch64-apple-ios-macabi.tar.xz": "0e1760828f4e0fa1cde0061ba5680619dcc1cdcafec9242cc18dc4547c73b1cd", + "rust-std-1.95.0-aarch64-apple-ios-sim.tar.xz": "4bfe5b0c74c10d121a8ac60f1833c7714b963f9130f6256ca313d94405267deb", + "rust-std-1.95.0-aarch64-apple-ios.tar.xz": "6fcc42d8dbba4910a128ffa32d62a730339a7e3882a90341a881f2edf66ff55a", + "rust-std-1.95.0-aarch64-linux-android.tar.xz": "de5e8fa5d955809891eea77682811fc90be705f78883bd94071e98f5a738d05b", + "rust-std-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "93d810b8872771afe04f66aa30a4eee48736aca693186e4c7cc2766e1a82e340", + "rust-std-1.95.0-aarch64-pc-windows-msvc.tar.xz": "be21b5a8a71c49b4dcbc19956233b0de7bfda3ee3c8a199148299f867e95cb42", + "rust-std-1.95.0-aarch64-unknown-fuchsia.tar.xz": "b311a0523f75e031d683d983515edb9baaf22a843dbd44ce2a30a3204752f592", + "rust-std-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "3a21b271b1ff973b94d69b25e7a39992f9fbcae1ab6d9475844a23e6ad3908ac", + "rust-std-1.95.0-aarch64-unknown-linux-musl.tar.xz": "f6710416ed9a7d5cf2a15efa761eb79a1deeb43f9961bbe05cc97bec4ef9064a", + "rust-std-1.95.0-aarch64-unknown-none-softfloat.tar.xz": "54d691468e25e7989b022a171337beadf78b5202877b312b75182b7f93efbb8b", + "rust-std-1.95.0-aarch64-unknown-none.tar.xz": "2b0c986dc9902866311f1fe2d44bc2bd84479d2ac84ed7ada76a5eb7ba37080f", + "rust-std-1.95.0-aarch64-unknown-uefi.tar.xz": "ca657564103024d345ca32e8e4ade7ebb395a51163d2897d9e5f8373c025e49e", + "rust-std-1.95.0-arm-linux-androideabi.tar.xz": "12a9c5fa24608159c2b2bd50abc0c6d0add407c0258cee894c2f61c07051a9c4", + "rust-std-1.95.0-arm-unknown-linux-gnueabi.tar.xz": "f6cd592dacdf41f724ee90a2f34db028e37ca2a7fb26fa86e93e8fd68e24d066", + "rust-std-1.95.0-arm-unknown-linux-gnueabihf.tar.xz": "fda8408ea17881c6529e27e58672d6c628f786cad557fac92856077e7a610239", + "rust-std-1.95.0-arm-unknown-linux-musleabi.tar.xz": "62f21fabc209fd0de53156764fc426da74c59525bb60cb4c1c3ffd1be0bbe00b", + "rust-std-1.95.0-arm-unknown-linux-musleabihf.tar.xz": "2842ce67f7a4c68c6e8b30ad3bb36484fb745edcc2694b2a36bf0609cf758044", + "rust-std-1.95.0-armv7-linux-androideabi.tar.xz": "61e95e144986c52ff9fa2fe3c249a68b2bf268adb2a2eeb81d80c180e43027f1", + "rust-std-1.95.0-armv7-unknown-linux-gnueabi.tar.xz": "e23454d6ca7fc3f5eb7cf9241572765176d86e9f45d4f394de31a5fd794e523f", + "rust-std-1.95.0-armv7-unknown-linux-gnueabihf.tar.xz": "bd319e18ca2dad0450f76277874d56356330da536be8cf271509f8e6f28ac6de", + "rust-std-1.95.0-armv7-unknown-linux-musleabi.tar.xz": "a49bc987e0531800f92825ce61402b5734ea747caa8970d3373506742ed7daa6", + "rust-std-1.95.0-armv7-unknown-linux-musleabihf.tar.xz": "77f9aaff4669c076edc96cdb99dc21749d2c692c862232b52176572c289fe671", + "rust-std-1.95.0-i686-linux-android.tar.xz": "e7b5e18d1d4119c7c1454ee8427933f27aeff9e81a22248c2829f5f299d3a937", + "rust-std-1.95.0-i686-pc-windows-gnu.tar.xz": "2e98fb94fc500690d7e72e071ce29dd98790ff518163963f5c82c32afba9231d", + "rust-std-1.95.0-i686-pc-windows-gnullvm.tar.xz": "b429a8c456d6a1815ece064102bd6a25f67d46db4f6d7b79dbe7d1b9d1f78e0f", + "rust-std-1.95.0-i686-pc-windows-msvc.tar.xz": "6206fd7e8bd119e9d1ba1c425ad25c282512eb0d5659f2dcb4be224b84715706", + "rust-std-1.95.0-i686-unknown-freebsd.tar.xz": "3b13b3ccecf482c0da3e94f19e0aa5e8e375622f4e09b30e6dae2b7638bee63f", + "rust-std-1.95.0-i686-unknown-linux-gnu.tar.xz": "527c5d5249a7f77b48d3c9da3ac512d27b47f43d08dbe3c6f82a3d5b35d8aa27", + "rust-std-1.95.0-i686-unknown-linux-musl.tar.xz": "af4d3e7aabb63d39a7a2ff5435cc993b65ff38a2d2e23f1967e519037a1b0455", + "rust-std-1.95.0-i686-unknown-uefi.tar.xz": "3233985273616ec36861f2d50b4a025c903b2bb8c45b171c0ae9e2de8342125b", + "rust-std-1.95.0-powerpc-unknown-linux-gnu.tar.xz": "59e0abbaa246502521e37c55b8d6cf88d5b8a697b0c70c61ec189937308f7246", + "rust-std-1.95.0-powerpc64-unknown-linux-gnu.tar.xz": "cc7fb9aa289ff1756502ae16a05e2885289165f01ed94a7c2db6576b3dae74a6", + "rust-std-1.95.0-powerpc64le-unknown-linux-gnu.tar.xz": "2370d9266051a0b23346d42e43a00f91b2daff22a963fb03e28ae50cb0b76c50", + "rust-std-1.95.0-powerpc64le-unknown-linux-musl.tar.xz": "0362ba4ecfe0bb508f0d2b064c6fbbfe72604d5ae1989d6a8a7b4fe5ff1889f1", + "rust-std-1.95.0-riscv32imc-unknown-none-elf.tar.xz": "04befebf3b15372dacb7e6c0fcdab842c17a63ea58bf2f4cdccd7182ae9195c6", + "rust-std-1.95.0-riscv64gc-unknown-linux-gnu.tar.xz": "50fe7869e166bb4c990a0e1664366b1ffdbe669664b7663cd03c079bd0efdcac", + "rust-std-1.95.0-riscv64gc-unknown-linux-musl.tar.xz": "e01bdbf5d6fa3e529671d49e87ba81dc9612101144f3ee5a0e1de3c48f27b47c", + "rust-std-1.95.0-riscv64gc-unknown-none-elf.tar.xz": "a4cb7a1527f3b56a39464e5ca2b174a27b708b26d78e604735eb5ffd9ee4d20b", + "rust-std-1.95.0-s390x-unknown-linux-gnu.tar.xz": "31978c1286afff9a0bb7f01c2ae4a39f40727b6100a82b6d934f146b06cde510", + "rust-std-1.95.0-thumbv6m-none-eabi.tar.xz": "602ec023c4615fc1c2d78b688554d42fa525e07e861c052f406fd7a607e5d5ee", + "rust-std-1.95.0-thumbv7em-none-eabi.tar.xz": "fb671966ba9aede333956ed43fcfe114ec890ca6e70369c9f3219871ee3ae8ae", + "rust-std-1.95.0-thumbv7em-none-eabihf.tar.xz": "fa3d189c09b64d818ad65a3fec1ce1c7d7b3908aea6fc4607a3fcc05067cad81", + "rust-std-1.95.0-thumbv7m-none-eabi.tar.xz": "e9e39f483ad4c1ce55fa4f508009e8d7d7ef25efe6592f7bfbabc159a24658ff", + "rust-std-1.95.0-thumbv8m.main-none-eabi.tar.xz": "3b64bffb193b37e83dc7b169add55f0ef3298220f8475468be4bb87276a68105", + "rust-std-1.95.0-thumbv8m.main-none-eabihf.tar.xz": "25ec92187d3a45fb1e397b0ba6000341d872523925a99a48b1978bdf1e6038cb", + "rust-std-1.95.0-wasm32-unknown-emscripten.tar.xz": "967b92a8682e8b8a1a459d776b36e41c906cdcac1008fef70e4800fec40d6864", + "rust-std-1.95.0-wasm32-unknown-unknown.tar.xz": "5587b89ff69623d09e476439d44a24453b4e4ea3d5e0b53a5c0a935151ff3fd1", + "rust-std-1.95.0-wasm32-wasip1-threads.tar.xz": "9079935a00a3c3aaf284957bbe82972983ce2708019687cec1f4988c30c1e0f3", + "rust-std-1.95.0-wasm32-wasip1.tar.xz": "86e5b6d98c7520bb9c3ad4f8cbbbf14beaf230b0f06b437db398e3c4f7dae43e", + "rust-std-1.95.0-wasm32-wasip2.tar.xz": "68146eb4c887431379966efa21d75dc957f18bd1166239c1deaa53fe38cb9ab4", + "rust-std-1.95.0-x86_64-apple-darwin.tar.xz": "2be13c14122b8d4d09b7f7c434fca9ae7215ec72049944189c88c4d9128ce504", + "rust-std-1.95.0-x86_64-apple-ios-macabi.tar.xz": "60b92e51e87f84046e0b19ccedc88c45b4b62c3ab10351f8473453f341c894f1", + "rust-std-1.95.0-x86_64-apple-ios.tar.xz": "11abb7b1c92b5a88b8c3ba21ee596a1be7dee5e817b336f26b4968d8ed5513ad", + "rust-std-1.95.0-x86_64-linux-android.tar.xz": "77b8e2be4a6e784a63cd77de944864c8044ddf4d5c7d56f663ada8a38a8319c4", + "rust-std-1.95.0-x86_64-pc-windows-gnu.tar.xz": "f57e045016a04130125fb43295d95f9ad2bebc296150eadb031dbf5167ad12bd", + "rust-std-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "3c52a0e34e0b4abe4439cabd77383408f5f9c80b6e249fbc0855df424b45008d", + "rust-std-1.95.0-x86_64-pc-windows-msvc.tar.xz": "7c659bdc88646e7e1befa370881bd311be87b26f006933a28b40dcab2f7cc832", + "rust-std-1.95.0-x86_64-unknown-freebsd.tar.xz": "dfe913c2f477172db10d3723d9f5c536d8b6f42776c979cc855820d8249adec3", + "rust-std-1.95.0-x86_64-unknown-fuchsia.tar.xz": "faa2bc09a3992f1d81b538121d64a3a396f4ec666e665c79d2ab47461c2d3206", + "rust-std-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "047ea7098803d3500fa1072e9cee5392697e21525559e4458128a2bf874aa382", + "rust-std-1.95.0-x86_64-unknown-linux-musl.tar.xz": "aee540abf132920f791ef781489851a078d69dff493fb628d49c1d573f92bb3a", + "rust-std-1.95.0-x86_64-unknown-netbsd.tar.xz": "7a82b71c53f20cb147a340819fcab645da220b312a96194531e631ee99783a7d", + "rust-std-1.95.0-x86_64-unknown-none.tar.xz": "7c151c0e7bf3b0b4d7136774cd3686e5f691b761b648b17e83af58e7669d3e01", + "rust-std-1.95.0-x86_64-unknown-uefi.tar.xz": "4cc55629480aa8ab5b39eb6b7458433b48461d6626fdea0330fb88e23af818ea", + "rustc-1.95.0-aarch64-apple-darwin.tar.xz": "149e85a285b6eba58eb6c8bdf7deb1b93763890598e62cb635a712e3a8454f04", + "rustc-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "758e729faabd8dabfab584d2bead59ca4fbcf1125ffc3c43a69332d6d9f2316b", + "rustc-1.95.0-aarch64-pc-windows-msvc.tar.xz": "0dbec9739b93427ccdd3948c3b1f83cec42e4c9545d930a8d1e1464ff4092c5f", + "rustc-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "0fe3689eeaed603e5ef24572d11597d3edadaefd2cb181674ad621260f2501d2", + "rustc-1.95.0-x86_64-apple-darwin.tar.xz": "33db457715446a69ed6f69f78f5fbb9ca8e17a16585d1d7a0060479bfe4c7afc", + "rustc-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "44ffbe057bb8f967087a1ad549a7139e9e5017d3aab396f42a6393f897e39531", + "rustc-1.95.0-x86_64-pc-windows-msvc.tar.xz": "4cb1f3b578adc6541cbe13a6f85f1fd8c0ce643d90b506a36dee24c680864c67", + "rustc-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "8426a3d170a5879f5682f5fbdd024a1779b3951e7baba685af2d6dc32a6dfc15", + "rustfmt-1.95.0-aarch64-apple-darwin.tar.xz": "c54af79adfdc790d27fc56e24407e370c80be5a89ad537eef9fbd45d3c3e28e8", + "rustfmt-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "85b690b664ee4f03521c6bc3f1af8fd7e6efae5851a8a7ff18965594484688e2", + "rustfmt-1.95.0-aarch64-pc-windows-msvc.tar.xz": "a873c048743e6da29e09a8b55c774ee113f8f6ae4fd57d988d304a6453801b34", + "rustfmt-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "64cce868f0f3d29f1524e11e9bab01ac9d538a31665fea1cd6b78af46a1c0a41", + "rustfmt-1.95.0-x86_64-apple-darwin.tar.xz": "5f7228f40a160e80d260e74e068d6fec8627aa02f1f5ae29d2019b9347076401", + "rustfmt-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "74eb0bb0227af0df81a5df9cbf8384e5e038c4cd575152ecc27dfa891ff3756a", + "rustfmt-1.95.0-x86_64-pc-windows-msvc.tar.xz": "8bcf91606e36b8a0164efafde50709cd7a3c02143a2edaa81dbf3dccd6ed8f4c", + "rustfmt-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "f1b2a7301513ffdd95ebf22ebbdd932e4d17fc806f748d93924740d0297b1396" } } } diff --git a/codex-rs/.github/workflows/cargo-audit.yml b/codex-rs/.github/workflows/cargo-audit.yml index 0c41471b65..1d935d61e8 100644 --- a/codex-rs/.github/workflows/cargo-audit.yml +++ b/codex-rs/.github/workflows/cargo-audit.yml @@ -17,7 +17,7 @@ jobs: working-directory: codex-rs steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Install cargo-audit uses: taiki-e/install-action@v2 with: diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1fd5e5f9e0..fd90b5ec21 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -181,7 +181,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -193,7 +193,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -207,7 +207,7 @@ dependencies = [ "bech32", "chacha20poly1305", "cookie-factory", - "hmac", + "hmac 0.12.1", "i18n-embed", "i18n-embed-fl", "lazy_static", @@ -216,7 +216,7 @@ dependencies = [ "rand 0.8.5", "rust-embed", "scrypt", - "sha2", + "sha2 0.10.9", "subtle", "x25519-dalek", "zeroize", @@ -231,12 +231,12 @@ dependencies = [ "base64 0.21.7", "chacha20poly1305", "cookie-factory", - "hkdf", + "hkdf 0.12.4", "io_tee", "nom 7.1.3", "rand 0.8.5", "secrecy", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -776,7 +776,7 @@ dependencies = [ "p256", "rand 0.8.5", "ring", - "sha2", + "sha2 0.10.9", "time", "tokio", "tracing", @@ -946,11 +946,11 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.12.1", "http 0.2.12", "http 1.4.0", "percent-encoding", - "sha2", + "sha2 0.10.9", "time", "tracing", ] @@ -1147,7 +1147,6 @@ dependencies = [ "axum-core", "base64 0.22.1", "bytes", - "form_urlencoded", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1163,15 +1162,13 @@ dependencies = [ "serde_core", "serde_json", "serde_path_to_error", - "serde_urlencoded", - "sha1", + "sha1 0.10.6", "sync_wrapper", "tokio", "tokio-tungstenite", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -1190,7 +1187,6 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -1328,7 +1324,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1340,6 +1336,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -1613,7 +1618,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1623,7 +1639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1666,7 +1682,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -1759,6 +1775,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "cmp_any" version = "0.8.1" @@ -1804,10 +1826,10 @@ dependencies = [ "jsonwebtoken", "pretty_assertions", "rand 0.9.3", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1825,7 +1847,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tokio", "tracing", ] @@ -1858,7 +1880,8 @@ dependencies = [ "http 1.4.0", "pretty_assertions", "regex-lite", - "reqwest", + "reqwest 0.12.28", + "schemars 0.8.22", "serde", "serde_json", "tempfile", @@ -1890,7 +1913,7 @@ dependencies = [ "codex-arg0", "codex-backend-client", "codex-chatgpt", - "codex-cloud-requirements", + "codex-cloud-config", "codex-config", "codex-core", "codex-core-plugins", @@ -1905,6 +1928,7 @@ dependencies = [ "codex-git-utils", "codex-guardian", "codex-hooks", + "codex-image-generation-extension", "codex-login", "codex-mcp", "codex-memories-extension", @@ -1927,19 +1951,21 @@ dependencies = [ "codex-utils-cli", "codex-utils-json-to-toml", "codex-utils-pty", + "codex-web-search-extension", + "codex-windows-sandbox", "core_test_support", "flate2", "futures", - "hmac", + "hmac 0.12.1", "opentelemetry", "opentelemetry_sdk", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "rmcp", "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "shlex", "tar", "tempfile", @@ -1997,10 +2023,10 @@ dependencies = [ "futures", "libc", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tokio", "tokio-tungstenite", @@ -2076,13 +2102,13 @@ dependencies = [ "constant_time_eq 0.3.1", "futures", "gethostname", - "hmac", + "hmac 0.12.1", "jsonwebtoken", "owo-colors", "pretty_assertions", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "time", "tokio", @@ -2166,7 +2192,7 @@ dependencies = [ "codex-model-provider", "codex-protocol", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", ] @@ -2281,6 +2307,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "unicode-segmentation", + "url", "which 8.0.0", "windows-sys 0.52.0", ] @@ -2301,7 +2328,7 @@ dependencies = [ "pretty_assertions", "rand 0.9.3", "rcgen", - "reqwest", + "reqwest 0.12.28", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -2317,7 +2344,7 @@ dependencies = [ ] [[package]] -name = "codex-cloud-requirements" +name = "codex-cloud-config" version = "0.0.0" dependencies = [ "async-trait", @@ -2330,11 +2357,11 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-utils-absolute-path", - "hmac", + "hmac 0.12.1", "pretty_assertions", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -2363,7 +2390,7 @@ dependencies = [ "owo-colors", "pretty_assertions", "ratatui", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "supports-color 3.0.2", @@ -2403,8 +2430,6 @@ dependencies = [ name = "codex-code-mode" version = "0.0.0" dependencies = [ - "async-channel", - "async-trait", "codex-protocol", "deno_core_icudata", "pretty_assertions", @@ -2452,7 +2477,7 @@ dependencies = [ "serde_ignored", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -2477,7 +2502,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tempfile", "tokio", "tracing", @@ -2526,6 +2551,7 @@ dependencies = [ "codex-network-proxy", "codex-otel", "codex-plugin", + "codex-prompts", "codex-protocol", "codex-response-debug-context", "codex-rmcp-client", @@ -2550,7 +2576,6 @@ dependencies = [ "codex-utils-pty", "codex-utils-stream-parser", "codex-utils-string", - "codex-utils-template", "codex-windows-sandbox", "core_test_support", "csv", @@ -2574,12 +2599,12 @@ dependencies = [ "pretty_assertions", "rand 0.9.3", "regex-lite", - "reqwest", + "reqwest 0.12.28", "rmcp", "serde", "serde_json", "serial_test", - "sha1", + "sha1 0.10.6", "shlex", "similar", "tempfile", @@ -2599,7 +2624,7 @@ dependencies = [ "uuid", "walkdir", "which 8.0.0", - "whoami", + "whoami 1.6.1", "wiremock", "zstd 0.13.3", ] @@ -2645,9 +2670,10 @@ dependencies = [ "codex-utils-plugins", "dirs", "flate2", + "indexmap 2.13.0", "libc", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "semver", "serde", "serde_json", @@ -2693,18 +2719,6 @@ dependencies = [ "zip 2.4.2", ] -[[package]] -name = "codex-debug-client" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "codex-app-server-protocol", - "pretty_assertions", - "serde", - "serde_json", -] - [[package]] name = "codex-exec" version = "0.0.0" @@ -2716,7 +2730,7 @@ dependencies = [ "codex-app-server-protocol", "codex-apply-patch", "codex-arg0", - "codex-cloud-requirements", + "codex-cloud-config", "codex-config", "codex-core", "codex-feedback", @@ -2776,7 +2790,7 @@ dependencies = [ "http 1.4.0", "pretty_assertions", "prost 0.14.3", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serial_test", @@ -2869,7 +2883,7 @@ dependencies = [ "codex-utils-output-truncation", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -2973,6 +2987,7 @@ dependencies = [ "codex-protocol", "codex-state", "codex-tools", + "codex-utils-template", "pretty_assertions", "serde", "serde_json", @@ -3014,6 +3029,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "codex-image-generation-extension" +version = "0.0.0" +dependencies = [ + "async-trait", + "codex-api", + "codex-core", + "codex-extension-api", + "codex-features", + "codex-login", + "codex-model-provider", + "codex-model-provider-info", + "codex-protocol", + "codex-tools", + "http 1.4.0", + "pretty_assertions", + "schemars 0.8.22", + "serde", + "serde_json", +] + [[package]] name = "codex-install-context" version = "0.0.0" @@ -3050,7 +3086,7 @@ dependencies = [ "seccompiler", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tokio", "url", @@ -3062,7 +3098,7 @@ version = "0.0.0" dependencies = [ "codex-core", "codex-model-provider-info", - "reqwest", + "reqwest 0.12.28", "serde_json", "tokio", "tracing", @@ -3096,11 +3132,11 @@ dependencies = [ "pretty_assertions", "rand 0.9.3", "regex-lite", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tiny_http", @@ -3135,7 +3171,7 @@ dependencies = [ "rmcp", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tempfile", "thiserror 2.0.18", "tokio", @@ -3343,6 +3379,7 @@ dependencies = [ "rustls-native-certs", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "time", @@ -3362,7 +3399,7 @@ dependencies = [ "codex-model-provider-info", "futures", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "semver", "serde_json", "tokio", @@ -3390,7 +3427,7 @@ dependencies = [ "opentelemetry_sdk", "os_info", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "strum_macros 0.28.0", @@ -3420,6 +3457,19 @@ dependencies = [ "pretty_assertions", ] +[[package]] +name = "codex-prompts" +version = "0.0.0" +dependencies = [ + "anyhow", + "codex-execpolicy", + "codex-git-utils", + "codex-protocol", + "codex-utils-absolute-path", + "codex-utils-template", + "pretty_assertions", +] + [[package]] name = "codex-protocol" version = "0.0.0" @@ -3442,7 +3492,7 @@ dependencies = [ "landlock", "pretty_assertions", "quick-xml", - "reqwest", + "reqwest 0.12.28", "schemars 0.8.22", "seccompiler", "serde", @@ -3490,7 +3540,7 @@ dependencies = [ "ctor 0.6.3", "libc", "pretty_assertions", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tiny_http", @@ -3518,12 +3568,12 @@ dependencies = [ "keyring", "oauth2", "pretty_assertions", - "reqwest", + "reqwest 0.13.4", "rmcp", "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "sse-stream", "tempfile", "thiserror 2.0.18", @@ -3549,7 +3599,6 @@ dependencies = [ "codex-protocol", "codex-state", "codex-utils-path", - "codex-utils-string", "pretty_assertions", "regex", "serde", @@ -3559,6 +3608,7 @@ dependencies = [ "tokio", "tracing", "uuid", + "zstd 0.13.3", ] [[package]] @@ -3614,7 +3664,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tracing", ] @@ -3791,9 +3841,8 @@ dependencies = [ "codex-app-server-client", "codex-app-server-protocol", "codex-arg0", - "codex-chatgpt", "codex-cli", - "codex-cloud-requirements", + "codex-cloud-config", "codex-config", "codex-connectors", "codex-core-plugins", @@ -3853,12 +3902,12 @@ dependencies = [ "ratatui", "ratatui-macros", "regex-lite", - "reqwest", + "reqwest 0.12.28", "rmcp", "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "shlex", "strum 0.27.2", "strum_macros 0.28.0", @@ -3926,7 +3975,7 @@ name = "codex-utils-cache" version = "0.0.0" dependencies = [ "lru 0.16.3", - "sha1", + "sha1 0.10.6", "tokio", ] @@ -4120,6 +4169,27 @@ dependencies = [ "v8", ] +[[package]] +name = "codex-web-search-extension" +version = "0.0.0" +dependencies = [ + "async-trait", + "codex-api", + "codex-core", + "codex-extension-api", + "codex-features", + "codex-login", + "codex-model-provider", + "codex-model-provider-info", + "codex-protocol", + "codex-tools", + "http 1.4.0", + "pretty_assertions", + "schemars 0.8.22", + "serde_json", + "url", +] + [[package]] name = "codex-windows-sandbox" version = "0.0.0" @@ -4243,7 +4313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -4404,7 +4474,7 @@ dependencies = [ "opentelemetry_sdk", "pretty_assertions", "regex-lite", - "reqwest", + "reqwest 0.12.28", "serde_json", "shlex", "similar", @@ -4449,7 +4519,7 @@ dependencies = [ "core-foundation-sys", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.21.1", "js-sys", "libc", "mach2", @@ -4471,6 +4541,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -4598,6 +4677,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto_box" version = "0.9.1" @@ -4675,6 +4763,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -4682,9 +4779,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -4916,10 +5013,10 @@ dependencies = [ "cbc", "dbus", "fastrand", - "hkdf", + "hkdf 0.12.4", "num", "once_cell", - "sha2", + "sha2 0.10.9", "zeroize", ] @@ -5109,12 +5206,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "diplomat" version = "0.15.0" @@ -5332,7 +5440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -5358,7 +5466,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -5380,7 +5488,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -5529,13 +5637,12 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -5791,17 +5898,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[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" @@ -5908,9 +6004,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -5918,15 +6014,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -5946,9 +6042,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -5965,9 +6061,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -5976,21 +6072,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -6000,7 +6096,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -6080,11 +6175,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -7142,11 +7251,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -7161,7 +7270,7 @@ dependencies = [ "http 1.4.0", "httpdate", "mime", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -7259,7 +7368,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -7268,7 +7386,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -7364,6 +7491,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -7402,7 +7538,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots", ] [[package]] @@ -7719,6 +7855,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -8121,19 +8263,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.114", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.114", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -8275,9 +8466,12 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" @@ -8323,9 +8517,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -8339,7 +8533,7 @@ source = "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df dependencies = [ "cxx", "glib", - "jni", + "jni 0.21.1", "js-sys", "lazy_static", "livekit-protocol", @@ -8629,12 +8823,12 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] @@ -8780,7 +8974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ "bitflags 2.10.0", - "jni-sys", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -8799,7 +8993,7 @@ version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -8964,22 +9158,6 @@ 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" @@ -9044,7 +9222,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -9099,11 +9276,11 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.5", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "url", ] @@ -9294,7 +9471,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni", + "jni 0.21.1", "ndk", "ndk-context", "num-derive", @@ -9460,7 +9637,7 @@ dependencies = [ "bytes", "http 1.4.0", "opentelemetry", - "reqwest", + "reqwest 0.12.28", ] [[package]] @@ -9475,7 +9652,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost 0.14.3", - "reqwest", + "reqwest 0.12.28", "serde_json", "thiserror 2.0.18", "tokio", @@ -9590,7 +9767,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9712,10 +9889,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", "password-hash", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9724,8 +9901,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -9826,17 +10003,6 @@ 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" @@ -9899,7 +10065,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -10136,7 +10302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.11.0", "log", "multimap", @@ -10156,7 +10322,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap", @@ -10308,6 +10474,7 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -10352,6 +10519,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -10515,7 +10688,7 @@ dependencies = [ "rama-utils", "rand 0.9.3", "serde", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -10568,7 +10741,7 @@ checksum = "b28ee9e1e5d39264414b71f5c33e7fbb66b382c3fac456fe0daad39cf5509933" dependencies = [ "ahash", "const_format", - "flume 0.12.0", + "flume", "hex", "ipnet", "itertools 0.14.0", @@ -10583,7 +10756,7 @@ dependencies = [ "rama-macros", "rama-utils", "serde", - "sha2", + "sha2 0.10.9", "socket2 0.6.2", "tokio", ] @@ -10636,7 +10809,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "webpki-roots 1.0.5", + "webpki-roots", "x509-parser", ] @@ -10703,6 +10876,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -10741,6 +10925,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -10960,9 +11150,49 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", "web-sys", - "webpki-roots 1.0.5", ] [[package]] @@ -10987,7 +11217,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -11007,12 +11237,11 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.15.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bef41ebc9ebed2c1b1d90203e9d1756091e8a00bbc3107676151f39868ca0ee" +checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e" dependencies = [ "async-trait", - "axum", "base64 0.22.1", "bytes", "chrono", @@ -11024,8 +11253,8 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "rand 0.9.3", - "reqwest", + "rand 0.10.1", + "reqwest 0.13.4", "rmcp-macros", "schemars 1.2.1", "serde", @@ -11043,9 +11272,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.15.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e88ad84b8b6237a934534a62b379a5be6388915663c0cc598ceb9b3292bbbfe" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -11054,26 +11283,6 @@ dependencies = [ "syn 2.0.114", ] -[[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 = "rtrb" version = "0.3.3" @@ -11115,7 +11324,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -11229,6 +11438,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -11441,7 +11677,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2 0.12.2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -11492,12 +11728,12 @@ dependencies = [ "cbc", "futures-util", "generic-array", - "hkdf", + "hkdf 0.12.4", "num", "once_cell", "rand 0.8.5", "serde", - "sha2", + "sha2 0.10.9", "zbus", ] @@ -11566,7 +11802,7 @@ checksum = "2f925d575b468e88b079faf590a8dd0c9c99e2ec29e9bab663ceb8b45056312f" dependencies = [ "httpdate", "native-tls", - "reqwest", + "reqwest 0.12.28", "sentry-actix", "sentry-backtrace", "sentry-contexts", @@ -11888,8 +12124,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -11898,8 +12145,8 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ - "digest", - "sha1", + "digest 0.10.7", + "sha1 0.10.6", ] [[package]] @@ -11915,8 +12162,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -11987,7 +12245,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -11997,6 +12255,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -12099,9 +12367,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -12112,12 +12380,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64 0.22.1", "bytes", + "cfg-if", "chrono", "crc", "crossbeam-queue", @@ -12127,17 +12396,16 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hashlink", "indexmap 2.13.0", "log", "memchr", - "once_cell", "percent-encoding", "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "time", @@ -12146,14 +12414,14 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -12164,79 +12432,64 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck 0.5.0", "hex", - "once_cell", "proc-macro2", "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "syn 2.0.114", + "thiserror 2.0.18", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "atoi", - "base64 0.22.1", "bitflags 2.10.0", "byteorder", "bytes", "chrono", "crc", - "digest", + "digest 0.11.3", "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", + "sha1 0.11.0", + "sha2 0.11.0", "sqlx-core", - "stringprep", "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64 0.22.1", @@ -12250,18 +12503,16 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf", - "hmac", - "home", + "hkdf 0.13.0", + "hmac 0.13.0", "itoa", "log", "md-5", "memchr", - "once_cell", - "rand 0.8.5", + "rand 0.10.1", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", @@ -12269,18 +12520,19 @@ dependencies = [ "time", "tracing", "uuid", - "whoami", + "whoami 2.1.2", ] [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "chrono", - "flume 0.11.1", + "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -12290,7 +12542,6 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror 2.0.18", "time", @@ -12688,7 +12939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -13521,7 +13772,7 @@ dependencies = [ "rand 0.9.3", "rustls", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "utf-8", ] @@ -13548,9 +13799,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" @@ -13683,7 +13934,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -13895,6 +14146,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -13960,6 +14220,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -13973,6 +14255,31 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -14070,7 +14377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.21.1", "log", "ndk-context", "objc2", @@ -14088,15 +14395,6 @@ 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.5", -] - [[package]] name = "webpki-roots" version = "1.0.5" @@ -14128,7 +14426,7 @@ dependencies = [ "anyhow", "fs2", "regex", - "reqwest", + "reqwest 0.12.28", "scratch", "semver", "zip 0.6.6", @@ -14174,6 +14472,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "widestring" version = "1.2.1" @@ -14805,6 +15109,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "wl-clipboard-rs" @@ -14977,7 +15363,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_repr", - "sha1", + "sha1 0.10.6", "static_assertions", "tracing", "uds_windows", @@ -15121,9 +15507,9 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", - "sha1", + "sha1 0.10.6", "time", "zstd 0.11.2+zstd.1.5.2", ] @@ -15144,12 +15530,12 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap 2.13.0", "lzma-rs", "memchr", "pbkdf2 0.12.2", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "time", "xz2", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 830e244082..cc20f8a3a8 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -14,7 +14,6 @@ members = [ "app-server-client", "app-server-protocol", "app-server-test-client", - "debug-client", "apply-patch", "arg0", "feedback", @@ -22,7 +21,7 @@ members = [ "install-context", "codex-backend-openapi-models", "code-mode", - "cloud-requirements", + "cloud-config", "cloud-tasks", "cloud-tasks-client", "cloud-tasks-mock-client", @@ -47,7 +46,9 @@ members = [ "ext/extension-api", "ext/goal", "ext/guardian", + "ext/image-generation", "ext/memories", + "ext/web-search", "external-agent-migration", "external-agent-sessions", "keyring-store", @@ -67,6 +68,7 @@ members = [ "process-hardening", "protocol", "realtime-webrtc", + "prompts", "rollout", "rollout-trace", "rmcp-client", @@ -147,7 +149,7 @@ codex-chatgpt = { path = "chatgpt" } codex-cli = { path = "cli" } codex-client = { path = "codex-client" } codex-collaboration-mode-templates = { path = "collaboration-mode-templates" } -codex-cloud-requirements = { path = "cloud-requirements" } +codex-cloud-config = { path = "cloud-config" } codex-cloud-tasks-client = { path = "cloud-tasks-client" } codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" } codex-code-mode = { path = "code-mode" } @@ -164,6 +166,7 @@ codex-execpolicy = { path = "execpolicy" } codex-extension-api = { path = "ext/extension-api" } codex-goal-extension = { path = "ext/goal" } codex-guardian = { path = "ext/guardian" } +codex-image-generation-extension = { path = "ext/image-generation" } codex-external-agent-migration = { path = "external-agent-migration" } codex-external-agent-sessions = { path = "external-agent-sessions" } codex-experimental-api-macros = { path = "codex-experimental-api-macros" } @@ -180,6 +183,7 @@ codex-lmstudio = { path = "lmstudio" } codex-login = { path = "login" } codex-message-history = { path = "message-history" } codex-memories-extension = { path = "ext/memories" } +codex-web-search-extension = { path = "ext/web-search" } codex-memories-read = { path = "memories/read" } codex-memories-write = { path = "memories/write" } codex-mcp = { path = "codex-mcp" } @@ -194,6 +198,7 @@ codex-model-provider = { path = "model-provider" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-realtime-webrtc = { path = "realtime-webrtc" } +codex-prompts = { path = "prompts" } codex-responses-api-proxy = { path = "responses-api-proxy" } codex-response-debug-context = { path = "response-debug-context" } codex-rmcp-client = { path = "rmcp-client" } @@ -340,7 +345,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [ regex = "1.12.3" regex-lite = "0.1.8" reqwest = { version = "0.12", features = ["cookies"] } -rmcp = { version = "0.15.0", default-features = false } +rmcp = { version = "1.7.0", default-features = false } runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" } rustls = { version = "0.23", default-features = false, features = [ "ring", @@ -364,13 +369,14 @@ sha2 = "0.10" shlex = "1.3.0" similar = "2.7.0" socket2 = "0.6.1" -sqlx = { version = "0.8.6", default-features = false, features = [ +sqlx = { version = "0.9.0", default-features = false, features = [ "chrono", "json", "macros", "migrate", - "runtime-tokio-rustls", - "sqlite", + "runtime-tokio", + "tls-rustls", + "sqlite-bundled", "time", "uuid", ] } diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index cabea80886..b3f6ba22ce 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -160,11 +160,13 @@ fn sample_thread_with_metadata( ephemeral: bool, source: AppServerSessionSource, thread_source: Option, + parent_thread_id: Option, ) -> Thread { Thread { id: thread_id.to_string(), session_id: format!("session-{thread_id}"), forked_from_id: None, + parent_thread_id, preview: "first prompt".to_string(), ephemeral, model_provider: "openai".to_string(), @@ -195,6 +197,7 @@ fn sample_thread_start_response( ephemeral, AppServerSessionSource::Exec, Some(AppServerThreadSource::User), + /*parent_thread_id*/ None, ), model: model.to_string(), model_provider: "openai".to_string(), @@ -240,6 +243,7 @@ fn sample_thread_resume_response( model, AppServerSessionSource::Exec, Some(AppServerThreadSource::User), + /*parent_thread_id*/ None, ) } @@ -249,9 +253,16 @@ fn sample_thread_resume_response_with_source( model: &str, source: AppServerSessionSource, thread_source: Option, + parent_thread_id: Option, ) -> ClientResponsePayload { ClientResponsePayload::ThreadResume(ThreadResumeResponse { - thread: sample_thread_with_metadata(thread_id, ephemeral, source, thread_source), + thread: sample_thread_with_metadata( + thread_id, + ephemeral, + source, + thread_source, + parent_thread_id, + ), model: model.to_string(), model_provider: "openai".to_string(), service_tier: None, @@ -263,6 +274,7 @@ fn sample_thread_resume_response_with_source( sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + initial_turns_page: None, }) } @@ -271,6 +283,7 @@ fn sample_turn_start_request(thread_id: &str, request_id: i64) -> ClientRequest request_id: RequestId::Integer(request_id), params: TurnStartParams { thread_id: thread_id.to_string(), + client_user_message_id: None, input: vec![ UserInput::Text { text: "hello".to_string(), @@ -390,6 +403,7 @@ fn sample_turn_steer_request( params: TurnSteerParams { thread_id: thread_id.to_string(), expected_turn_id: expected_turn_id.to_string(), + client_user_message_id: None, input: vec![ UserInput::Text { text: "more".to_string(), @@ -401,6 +415,7 @@ fn sample_turn_steer_request( }, ], responsesapi_client_metadata: None, + additional_context: None, }, } } @@ -1212,6 +1227,7 @@ fn compaction_event_serializes_expected_shape() { completed_at: 106, duration_ms: Some(6543), }, + "session-thread-1".to_string(), sample_app_server_client_metadata(), sample_runtime_metadata(), Some(ThreadSource::User), @@ -1228,6 +1244,7 @@ fn compaction_event_serializes_expected_shape() { "event_type": "codex_compaction_event", "event_params": { "thread_id": "thread-1", + "session_id": "session-thread-1", "turn_id": "turn-1", "app_server_client": { "product_client_id": DEFAULT_ORIGINATOR, @@ -1306,6 +1323,7 @@ fn thread_initialized_event_serializes_expected_shape() { event_type: "codex_thread_initialized", event_params: ThreadInitializedEventParams { thread_id: "thread-0".to_string(), + session_id: "session-thread-0".to_string(), app_server_client: CodexAppServerClientMetadata { product_client_id: DEFAULT_ORIGINATOR.to_string(), client_name: Some("codex-tui".to_string()), @@ -1337,6 +1355,7 @@ fn thread_initialized_event_serializes_expected_shape() { "event_type": "codex_thread_initialized", "event_params": { "thread_id": "thread-0", + "session_id": "session-thread-0", "app_server_client": { "product_client_id": DEFAULT_ORIGINATOR, "client_name": "codex-tui", @@ -1604,6 +1623,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_thread_initialized"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1"); assert_eq!( payload[0]["event_params"]["app_server_client"]["product_client_id"], DEFAULT_ORIGINATOR @@ -1746,6 +1766,7 @@ async fn compaction_event_ingests_custom_fact() { agent_role: None, }), Some(AppServerThreadSource::Subagent), + Some(parent_thread_id.to_string()), )), }, &mut events, @@ -1780,6 +1801,7 @@ async fn compaction_event_ingests_custom_fact() { let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_compaction_event"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1"); assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1"); assert_eq!(payload[0]["event_params"]["turn_id"], "turn-compact"); assert_eq!( @@ -1904,6 +1926,10 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_guardian_review"); + assert_eq!( + payload[0]["event_params"]["session_id"], + "session-thread-guardian" + ); assert_eq!(payload[0]["event_params"]["thread_id"], "thread-guardian"); assert_eq!(payload[0]["event_params"]["turn_id"], "turn-guardian"); assert_eq!(payload[0]["event_params"]["review_id"], "review-guardian"); @@ -2396,6 +2422,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() { fn subagent_thread_started_review_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2439,8 +2466,9 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() { .expect("valid thread id"); let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-spawn".to_string(), - parent_thread_id: None, + parent_thread_id: Some(parent_thread_id.to_string()), product_client_id: "codex-tui".to_string(), client_name: "codex-tui".to_string(), client_version: "1.0.0".to_string(), @@ -2458,18 +2486,21 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() { )); let payload = serde_json::to_value(&event).expect("serialize thread spawn subagent event"); + assert_eq!(payload["event_params"]["thread_id"], "thread-spawn"); assert_eq!(payload["event_params"]["thread_source"], "subagent"); assert_eq!(payload["event_params"]["subagent_source"], "thread_spawn"); assert_eq!( payload["event_params"]["parent_thread_id"], "11111111-1111-1111-1111-111111111111" ); + assert_eq!(payload["event_params"]["session_id"], "session-root"); } #[test] fn subagent_thread_started_memory_consolidation_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-memory".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2495,6 +2526,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() { fn subagent_thread_started_other_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-guardian".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2514,10 +2546,14 @@ fn subagent_thread_started_other_serializes_expected_shape() { #[test] fn subagent_thread_started_other_serializes_explicit_parent_thread_id() { + let parent_thread_id = + codex_protocol::ThreadId::from_string("33333333-3333-4333-8333-333333333333") + .expect("valid thread id"); let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-guardian".to_string(), - parent_thread_id: Some("parent-thread-guardian".to_string()), + parent_thread_id: Some(parent_thread_id.to_string()), product_client_id: "codex-tui".to_string(), client_name: "codex-tui".to_string(), client_version: "1.0.0".to_string(), @@ -2532,7 +2568,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() { assert_eq!(payload["event_params"]["subagent_source"], "guardian"); assert_eq!( payload["event_params"]["parent_thread_id"], - "parent-thread-guardian" + "33333333-3333-4333-8333-333333333333" ); } @@ -2545,6 +2581,7 @@ async fn subagent_thread_started_publishes_without_initialize() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2618,8 +2655,9 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), - parent_thread_id: None, + parent_thread_id: Some(parent_thread_id.to_string()), product_client_id: "parent-client".to_string(), client_name: "parent-client".to_string(), client_version: "1.0.0".to_string(), @@ -2665,6 +2703,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() { .await; let payload = serde_json::to_value(&events).expect("serialize events"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-root"); + assert_eq!(payload[0]["event_params"]["thread_id"], "thread-review"); assert_eq!( payload[0]["event_params"]["app_server_client"]["product_client_id"], "parent-client" @@ -2685,6 +2725,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-subagent".to_string(), parent_thread_id: Some("thread-1".to_string()), product_client_id: "codex-tui".to_string(), @@ -3190,6 +3231,7 @@ fn turn_event_serializes_expected_shape() { event_type: "codex_turn_event", event_params: crate::events::CodexTurnEventParams { thread_id: "thread-2".to_string(), + session_id: "session-thread-2".to_string(), turn_id: "turn-2".to_string(), app_server_client: sample_app_server_client_metadata(), runtime: sample_runtime_metadata(), @@ -3240,6 +3282,7 @@ fn turn_event_serializes_expected_shape() { "event_type": "codex_turn_event", "event_params": { "thread_id": "thread-2", + "session_id": "session-thread-2", "turn_id": "turn-2", "submission_type": null, "app_server_client": { @@ -3341,6 +3384,10 @@ async fn accepted_turn_steer_emits_expected_event() { let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event"); assert_eq!(payload["event_type"], json!("codex_turn_steer_event")); assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!( + payload["event_params"]["session_id"], + json!("session-thread-2") + ); assert_eq!(payload["event_params"]["expected_turn_id"], json!("turn-2")); assert_eq!(payload["event_params"]["accepted_turn_id"], json!("turn-2")); assert_eq!(payload["event_params"]["num_input_images"], json!(1)); @@ -3558,6 +3605,10 @@ async fn turn_lifecycle_emits_turn_event() { let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); assert_eq!(payload["event_type"], json!("codex_turn_event")); assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!( + payload["event_params"]["session_id"], + json!("session-thread-2") + ); assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( payload["event_params"]["app_server_client"], diff --git a/codex-rs/analytics/src/client_tests.rs b/codex-rs/analytics/src/client_tests.rs index bfb224d899..b7aa9f1c97 100644 --- a/codex-rs/analytics/src/client_tests.rs +++ b/codex-rs/analytics/src/client_tests.rs @@ -89,6 +89,7 @@ fn sample_turn_start_request() -> ClientRequest { request_id: RequestId::Integer(1), params: TurnStartParams { thread_id: "thread-1".to_string(), + client_user_message_id: None, input: Vec::new(), ..Default::default() }, @@ -101,8 +102,10 @@ fn sample_turn_steer_request() -> ClientRequest { params: TurnSteerParams { thread_id: "thread-1".to_string(), expected_turn_id: "turn-1".to_string(), + client_user_message_id: None, input: Vec::new(), responsesapi_client_metadata: None, + additional_context: None, }, } } @@ -121,6 +124,7 @@ fn sample_thread(thread_id: &str) -> Thread { id: thread_id.to_string(), session_id: format!("session-{thread_id}"), forked_from_id: None, + parent_thread_id: None, preview: "first prompt".to_string(), ephemeral: false, model_provider: "openai".to_string(), @@ -171,6 +175,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload { sandbox: AppServerSandboxPolicy::DangerFullAccess, active_permission_profile: None, reasoning_effort: None, + initial_turns_page: None, }) } diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 87cb165ac2..fd52fefc1e 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -147,6 +147,7 @@ pub(crate) struct CodexRuntimeMetadata { #[derive(Serialize)] pub(crate) struct ThreadInitializedEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, pub(crate) model: String, @@ -420,6 +421,7 @@ impl GuardianReviewAnalyticsResult { #[derive(Serialize)] pub(crate) struct GuardianReviewEventPayload { + pub(crate) session_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, #[serde(flatten)] @@ -738,6 +740,7 @@ pub(crate) struct CodexHookRunEventRequest { #[derive(Serialize)] pub(crate) struct CodexCompactionEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) turn_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, @@ -767,6 +770,7 @@ pub(crate) struct CodexCompactionEventRequest { #[derive(Serialize)] pub(crate) struct CodexTurnEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) turn_id: String, // TODO(rhan-oai): Populate once queued/default submission type is plumbed from // the turn/start callsites instead of always being reported as None. @@ -821,6 +825,7 @@ pub(crate) struct CodexTurnEventRequest { #[derive(Serialize)] pub(crate) struct CodexTurnSteerEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) expected_turn_id: Option, pub(crate) accepted_turn_id: Option, pub(crate) app_server_client: CodexAppServerClientMetadata, @@ -926,6 +931,7 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu pub(crate) fn codex_compaction_event_params( input: CodexCompactionEvent, + session_id: String, app_server_client: CodexAppServerClientMetadata, runtime: CodexRuntimeMetadata, thread_source: Option, @@ -934,6 +940,7 @@ pub(crate) fn codex_compaction_event_params( ) -> CodexCompactionEventParams { CodexCompactionEventParams { thread_id: input.thread_id, + session_id, turn_id: input.turn_id, app_server_client, runtime, @@ -1005,6 +1012,7 @@ fn analytics_hook_source(source: HookSource) -> &'static str { HookSource::SessionFlags => "session_flags", HookSource::Plugin => "plugin", HookSource::CloudRequirements => "cloud_requirements", + HookSource::CloudManagedConfig => "cloud_managed_config", HookSource::LegacyManagedConfigFile => "legacy_managed_config_file", HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm", HookSource::Unknown => "unknown", @@ -1026,6 +1034,7 @@ pub(crate) fn subagent_thread_started_event_request( ) -> ThreadInitializedEvent { let event_params = ThreadInitializedEventParams { thread_id: input.thread_id, + session_id: input.session_id, app_server_client: CodexAppServerClientMetadata { product_client_id: input.product_client_id, client_name: Some(input.client_name), @@ -1039,9 +1048,7 @@ pub(crate) fn subagent_thread_started_event_request( thread_source: Some(ThreadSource::Subagent), initialization_mode: ThreadInitializationMode::New, subagent_source: Some(subagent_source_name(&input.subagent_source)), - parent_thread_id: input - .parent_thread_id - .or_else(|| subagent_parent_thread_id(&input.subagent_source)), + parent_thread_id: input.parent_thread_id, created_at: input.created_at, }; ThreadInitializedEvent { @@ -1051,22 +1058,7 @@ pub(crate) fn subagent_thread_started_event_request( } pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String { - match subagent_source { - SubAgentSource::Review => "review".to_string(), - SubAgentSource::Compact => "compact".to_string(), - SubAgentSource::ThreadSpawn { .. } => "thread_spawn".to_string(), - SubAgentSource::MemoryConsolidation => "memory_consolidation".to_string(), - SubAgentSource::Other(other) => other.clone(), - } -} - -pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option { - match subagent_source { - SubAgentSource::ThreadSpawn { - parent_thread_id, .. - } => Some(parent_thread_id.to_string()), - _ => None, - } + subagent_source.kind().to_string() } fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus { diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 56bd0a5d2c..9a85276712 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -199,6 +199,7 @@ pub struct AppInvocation { #[derive(Clone)] pub struct SubAgentThreadStartedInput { + pub session_id: String, pub thread_id: String, pub parent_thread_id: Option, pub product_client_id: String, diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index d072720d10..f206393476 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -55,7 +55,6 @@ use crate::events::codex_hook_run_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; use crate::events::plugin_state_event_type; -use crate::events::subagent_parent_thread_id; use crate::events::subagent_source_name; use crate::events::subagent_thread_started_event_request; use crate::facts::AnalyticsFact; @@ -255,6 +254,7 @@ struct ItemReviewSummary { #[derive(Clone)] struct ThreadMetadataState { + session_id: String, thread_source: Option, initialization_mode: ThreadInitializationMode, subagent_source: Option, @@ -263,24 +263,24 @@ struct ThreadMetadataState { impl ThreadMetadataState { fn from_thread_metadata( + session_id: String, session_source: &SessionSource, thread_source: Option, + parent_thread_id: Option, initialization_mode: ThreadInitializationMode, ) -> Self { - let (subagent_source, parent_thread_id) = match session_source { - SessionSource::SubAgent(subagent_source) => ( - Some(subagent_source_name(subagent_source)), - subagent_parent_thread_id(subagent_source), - ), + let subagent_source = match session_source { + SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)), SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec | SessionSource::Mcp | SessionSource::Custom(_) | SessionSource::Internal(_) - | SessionSource::Unknown => (None, None), + | SessionSource::Unknown => None, }; Self { + session_id, thread_source, initialization_mode, subagent_source, @@ -513,10 +513,7 @@ impl AnalyticsReducer { input: SubAgentThreadStartedInput, out: &mut Vec, ) { - let parent_thread_id = input - .parent_thread_id - .clone() - .or_else(|| subagent_parent_thread_id(&input.subagent_source)); + let parent_thread_id = input.parent_thread_id.clone(); let parent_connection_id = parent_thread_id .as_ref() .and_then(|parent_thread_id| self.threads.get(parent_thread_id)) @@ -525,6 +522,7 @@ impl AnalyticsReducer { thread_state .metadata .get_or_insert_with(|| ThreadMetadataState { + session_id: input.session_id.clone(), thread_source: Some(ThreadSource::Subagent), initialization_mode: ThreadInitializationMode::New, subagent_source: Some(subagent_source_name(&input.subagent_source)), @@ -543,8 +541,8 @@ impl AnalyticsReducer { input: GuardianReviewEventParams, out: &mut Vec, ) { - let Some(connection_state) = - self.thread_connection_or_warn(AnalyticsDropSite::guardian(&input)) + let Some((connection_state, thread_metadata)) = + self.thread_context_or_warn(AnalyticsDropSite::guardian(&input)) else { return; }; @@ -552,6 +550,7 @@ impl AnalyticsReducer { GuardianReviewEventRequest { event_type: "codex_guardian_review", event_params: GuardianReviewEventPayload { + session_id: thread_metadata.session_id.clone(), app_server_client: connection_state.app_server_client.clone(), runtime: connection_state.runtime.clone(), guardian_review: input, @@ -1231,13 +1230,17 @@ impl AnalyticsReducer { out: &mut Vec, ) { let session_source: SessionSource = thread.source.into(); + let session_id = thread.session_id; let thread_id = thread.id; + let parent_thread_id = thread.parent_thread_id; let Some(connection_state) = self.connections.get(&connection_id) else { return; }; let thread_metadata = ThreadMetadataState::from_thread_metadata( + session_id.clone(), &session_source, thread.thread_source.map(Into::into), + parent_thread_id, initialization_mode, ); self.threads.insert( @@ -1252,6 +1255,7 @@ impl AnalyticsReducer { event_type: "codex_thread_initialized", event_params: ThreadInitializedEventParams { thread_id, + session_id, app_server_client: connection_state.app_server_client.clone(), runtime: connection_state.runtime.clone(), model, @@ -1277,6 +1281,7 @@ impl AnalyticsReducer { event_type: "codex_compaction_event", event_params: codex_compaction_event_params( input, + thread_metadata.session_id.clone(), connection_state.app_server_client.clone(), connection_state.runtime.clone(), thread_metadata.thread_source, @@ -1379,6 +1384,7 @@ impl AnalyticsReducer { event_type: "codex_turn_steer_event", event_params: CodexTurnSteerEventParams { thread_id: pending_request.thread_id, + session_id: thread_metadata.session_id.clone(), expected_turn_id: Some(pending_request.expected_turn_id), accepted_turn_id, app_server_client: connection_state.app_server_client.clone(), @@ -2447,6 +2453,7 @@ fn codex_turn_event_params( let token_usage = turn_state.token_usage.clone(); CodexTurnEventParams { thread_id, + session_id: thread_metadata.session_id.clone(), turn_id, app_server_client, runtime, diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index d2bec3d2ed..dc5dd6ad60 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -12,6 +12,28 @@ ], "type": "string" }, + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -1063,6 +1085,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -2964,6 +2988,20 @@ ], "type": "object" }, + "SkillsExtraRootsSetParams": { + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "type": "object" + }, "SkillsListParams": { "properties": { "cwds": { @@ -3519,6 +3557,42 @@ } ] }, + "ThreadResumeInitialTurnsPageParams": { + "properties": { + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + } + }, + "type": "object" + }, "ThreadResumeParams": { "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", "properties": { @@ -3925,6 +3999,12 @@ ], "description": "Override where approval requests are routed for review on this turn and subsequent turns." }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "cwd": { "description": "Override the working directory for this turn and subsequent turns.", "type": [ @@ -4011,6 +4091,12 @@ }, "TurnSteerParams": { "properties": { + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "expectedTurnId": { "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", "type": "string" @@ -4711,6 +4797,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 90899cb152..4eb5cae387 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2002,6 +2002,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" @@ -2031,6 +2032,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -3363,6 +3366,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -3559,6 +3569,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4c6d5a6eee..b97c1f8fb7 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -709,6 +709,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -5768,6 +5792,28 @@ ], "type": "string" }, + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/v2/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, "AdditionalFileSystemPermissions": { "properties": { "entries": { @@ -7557,6 +7603,33 @@ "title": "SystemConfigLayerSource", "type": "object" }, + { + "description": "Enterprise-managed config layer delivered by the cloud config bundle.", + "properties": { + "id": { + "description": "Stable identifier for the delivered layer.", + "type": "string" + }, + "name": { + "description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.", + "type": "string" + }, + "type": { + "enum": [ + "enterpriseManaged" + ], + "title": "EnterpriseManagedConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "EnterpriseManagedConfigLayerSource", + "type": "object" + }, { "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory", "properties": { @@ -7763,6 +7836,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/v2/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { @@ -10036,6 +10118,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" @@ -10138,6 +10221,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -10940,6 +11025,47 @@ "title": "McpResourceReadResponse", "type": "object" }, + "McpServerInfo": { + "description": "Presentation metadata advertised by an initialized MCP server.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, "McpServerMigration": { "properties": { "name": { @@ -11050,6 +11176,16 @@ }, "type": "array" }, + "serverInfo": { + "anyOf": [ + { + "$ref": "#/definitions/v2/McpServerInfo" + }, + { + "type": "null" + } + ] + }, "tools": { "additionalProperties": { "$ref": "#/definitions/v2/Tool" @@ -11770,7 +11906,7 @@ "NetworkUnixSocketPermission": { "enum": [ "allow", - "none" + "deny" ], "type": "string" }, @@ -15046,6 +15182,27 @@ "title": "SkillsConfigWriteResponse", "type": "object" }, + "SkillsExtraRootsSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" + }, + "SkillsExtraRootsSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" + }, "SkillsListEntry": { "properties": { "cwd": { @@ -15356,6 +15513,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -15921,6 +16085,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/v2/UserInput" @@ -17126,6 +17296,42 @@ "title": "ThreadRealtimeTranscriptDoneNotification", "type": "object" }, + "ThreadResumeInitialTurnsPageParams": { + "properties": { + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/v2/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + } + }, + "type": "object" + }, "ThreadResumeParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", @@ -18302,6 +18508,12 @@ ], "description": "Override where approval requests are routed for review on this turn and subsequent turns." }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "cwd": { "description": "Override the working directory for this turn and subsequent turns.", "type": [ @@ -18429,6 +18641,12 @@ "TurnSteerParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "expectedTurnId": { "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", "type": "string" @@ -18464,6 +18682,32 @@ "title": "TurnSteerResponse", "type": "object" }, + "TurnsPage": { + "properties": { + "backwardsCursor": { + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/v2/Turn" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "type": "object" + }, "UserInput": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index e74323745e..f8261c86d6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -164,6 +164,28 @@ ], "type": "string" }, + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, "AdditionalFileSystemPermissions": { "properties": { "entries": { @@ -1435,6 +1457,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -3926,6 +3972,33 @@ "title": "SystemConfigLayerSource", "type": "object" }, + { + "description": "Enterprise-managed config layer delivered by the cloud config bundle.", + "properties": { + "id": { + "description": "Stable identifier for the delivered layer.", + "type": "string" + }, + "name": { + "description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.", + "type": "string" + }, + "type": { + "enum": [ + "enterpriseManaged" + ], + "title": "EnterpriseManagedConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "EnterpriseManagedConfigLayerSource", + "type": "object" + }, { "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory", "properties": { @@ -4132,6 +4205,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { @@ -6516,6 +6598,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" @@ -6618,6 +6701,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -7469,6 +7554,47 @@ "title": "McpResourceReadResponse", "type": "object" }, + "McpServerInfo": { + "description": "Presentation metadata advertised by an initialized MCP server.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, "McpServerMigration": { "properties": { "name": { @@ -7579,6 +7705,16 @@ }, "type": "array" }, + "serverInfo": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerInfo" + }, + { + "type": "null" + } + ] + }, "tools": { "additionalProperties": { "$ref": "#/definitions/Tool" @@ -8299,7 +8435,7 @@ "NetworkUnixSocketPermission": { "enum": [ "allow", - "none" + "deny" ], "type": "string" }, @@ -12870,6 +13006,27 @@ "title": "SkillsConfigWriteResponse", "type": "object" }, + "SkillsExtraRootsSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" + }, + "SkillsExtraRootsSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" + }, "SkillsListEntry": { "properties": { "cwd": { @@ -13180,6 +13337,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -13745,6 +13909,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" @@ -14950,6 +15120,42 @@ "title": "ThreadRealtimeTranscriptDoneNotification", "type": "object" }, + "ThreadResumeInitialTurnsPageParams": { + "properties": { + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + } + }, + "type": "object" + }, "ThreadResumeParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", @@ -16126,6 +16332,12 @@ ], "description": "Override where approval requests are routed for review on this turn and subsequent turns." }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "cwd": { "description": "Override the working directory for this turn and subsequent turns.", "type": [ @@ -16253,6 +16465,12 @@ "TurnSteerParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "expectedTurnId": { "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", "type": "string" @@ -16288,6 +16506,32 @@ "title": "TurnSteerResponse", "type": "object" }, + "TurnsPage": { + "properties": { + "backwardsCursor": { + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "type": "object" + }, "UserInput": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 4a104b3bd5..e714c0975d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -498,6 +498,33 @@ "title": "SystemConfigLayerSource", "type": "object" }, + { + "description": "Enterprise-managed config layer delivered by the cloud config bundle.", + "properties": { + "id": { + "description": "Stable identifier for the delivered layer.", + "type": "string" + }, + "name": { + "description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.", + "type": "string" + }, + "type": { + "enum": [ + "enterpriseManaged" + ], + "title": "EnterpriseManagedConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "EnterpriseManagedConfigLayerSource", + "type": "object" + }, { "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 63a209cc77..0b8170d659 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -121,6 +121,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { @@ -460,7 +469,7 @@ "NetworkUnixSocketPermission": { "enum": [ "allow", - "none" + "deny" ], "type": "string" }, @@ -485,6 +494,13 @@ "live" ], "type": "string" + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json index 37816463b6..d1a2254a92 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigWriteResponse.json @@ -73,6 +73,33 @@ "title": "SystemConfigLayerSource", "type": "object" }, + { + "description": "Enterprise-managed config layer delivered by the cloud config bundle.", + "properties": { + "id": { + "description": "Stable identifier for the delivered layer.", + "type": "string" + }, + "name": { + "description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.", + "type": "string" + }, + "type": { + "enum": [ + "enterpriseManaged" + ], + "title": "EnterpriseManagedConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "EnterpriseManagedConfigLayerSource", + "type": "object" + }, { "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json index 088da47484..8684bf9ae5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json @@ -166,6 +166,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json index 38395cded9..5b2750d780 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json @@ -166,6 +166,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json index c58a5c767d..c3288ee0fd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json @@ -130,6 +130,7 @@ "sessionFlags", "plugin", "cloudRequirements", + "cloudManagedConfig", "legacyManagedConfigFile", "legacyManagedConfigMdm", "unknown" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 87d36d2927..8ba25f9a07 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -287,6 +287,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -498,6 +500,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index d45fddf4ba..70fc43f7b8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -287,6 +287,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -498,6 +500,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index fc181c2702..0dc2f5e2e4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -10,6 +10,47 @@ ], "type": "string" }, + "McpServerInfo": { + "description": "Presentation metadata advertised by an initialized MCP server.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, "McpServerStatus": { "properties": { "authStatus": { @@ -30,6 +71,16 @@ }, "type": "array" }, + "serverInfo": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerInfo" + }, + { + "type": "null" + } + ] + }, "tools": { "additionalProperties": { "$ref": "#/definitions/Tool" diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index e69b342282..f82acbae64 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -165,6 +165,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 877eeb1278..a644ce8c4e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -642,6 +644,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json new file mode 100644 index 0000000000..9992cdf8b4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json new file mode 100644 index 0000000000..cf0792c748 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 8c45f57d38..0d3a3fe8db 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -1034,6 +1036,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -1119,6 +1128,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index a9a186228b..a500c21587 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index a2a4834cb3..6e08edb962 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 5d76f9b181..eed031d1e4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index d4bdeda0ed..364cb36617 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -224,6 +224,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -981,6 +983,74 @@ "danger-full-access" ], "type": "string" + }, + "SortDirection": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "ThreadResumeInitialTurnsPageParams": { + "properties": { + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + } + }, + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] } }, "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 8055ff58fc..3a9e51f2a7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -1034,6 +1036,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -1119,6 +1128,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" @@ -1993,6 +2008,32 @@ ], "type": "string" }, + "TurnsPage": { + "properties": { + "backwardsCursor": { + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "type": "object" + }, "UserInput": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index d0e25a1eaa..9bc687fbfb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index d456ccebc4..8640c4e9ed 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -1034,6 +1036,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -1119,6 +1128,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index dd7de34b82..f7830e221e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 5929510520..9a634c1110 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -849,6 +851,13 @@ "null" ] }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, "path": { "description": "[UNSTABLE] Path to the thread on disk.", "type": [ @@ -934,6 +943,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index ddaab863f7..6c64eb3275 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -642,6 +644,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index ecea8f1997..f997976aca 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -5,6 +5,28 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", "enum": [ @@ -101,6 +123,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -492,6 +516,12 @@ ], "description": "Override where approval requests are routed for review on this turn and subsequent turns." }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "cwd": { "description": "Override the working directory for this turn and subsequent turns.", "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index a2836f7f03..5fe9754576 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -642,6 +644,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 8c57890a3b..1db1497218 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -642,6 +644,12 @@ "oneOf": [ { "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, "content": { "items": { "$ref": "#/definitions/UserInput" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json index 1b7cfbf400..63317f13ba 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json @@ -1,6 +1,28 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, "ByteRange": { "properties": { "end": { @@ -22,6 +44,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], @@ -194,6 +218,12 @@ } }, "properties": { + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, "expectedTurnId": { "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 86ba2cd171..e19ac2dfde 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -57,6 +57,7 @@ import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; +import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; @@ -85,4 +86,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts b/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts index 5a62cc32f1..a48f07c088 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ImageDetail = "high" | "original"; +export type ImageDetail = "auto" | "low" | "high" | "original"; diff --git a/codex-rs/app-server-protocol/schema/typescript/McpServerInfo.ts b/codex-rs/app-server-protocol/schema/typescript/McpServerInfo.ts new file mode 100644 index 0000000000..a3f6b0e142 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/McpServerInfo.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +/** + * Presentation metadata advertised by an initialized MCP server. + */ +export type McpServerInfo = { name: string, title: string | null, version: string, description: string | null, icons: Array | null, websiteUrl: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 8be75af546..458d2e43b9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -42,6 +42,7 @@ export type { InternalSessionSource } from "./InternalSessionSource"; export type { LocalShellAction } from "./LocalShellAction"; export type { LocalShellExecAction } from "./LocalShellExecAction"; export type { LocalShellStatus } from "./LocalShellStatus"; +export type { McpServerInfo } from "./McpServerInfo"; export type { MessagePhase } from "./MessagePhase"; export type { ModeKind } from "./ModeKind"; export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextEntry.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextEntry.ts new file mode 100644 index 0000000000..8d959269d4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextEntry.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AdditionalContextKind } from "./AdditionalContextKind"; + +export type AdditionalContextEntry = { value: string, kind: AdditionalContextKind, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextKind.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextKind.ts new file mode 100644 index 0000000000..cd60bd7a8d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalContextKind.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AdditionalContextKind = "untrusted" | "application"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts index 08cb8c6bfd..963c331b5d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigLayerSource.ts @@ -8,7 +8,17 @@ export type ConfigLayerSource = { "type": "mdm", domain: string, key: string, } * This is the path to the system config.toml file, though it is not * guaranteed to exist. */ -file: AbsolutePathBuf, } | { "type": "user", +file: AbsolutePathBuf, } | { "type": "enterpriseManaged", +/** + * Stable identifier for the delivered layer. + */ +id: string, +/** + * Admin-facing name for the delivered layer. This is surfaced in + * diagnostics so users know which cloud layer needs administrator + * attention. + */ +name: string, } | { "type": "user", /** * This is the path to the user's config.toml file, though it is not * guaranteed to exist. diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index c5f3895866..3e60e78da5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,5 +6,6 @@ import type { AskForApproval } from "./AskForApproval"; import type { ComputerUseRequirements } from "./ComputerUseRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HookSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HookSource.ts index 98bbe1e412..5c3cb2eabf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/HookSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HookSource.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown"; +export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "cloudManagedConfig" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts index 430494e268..d2e99ce96f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts @@ -1,9 +1,10 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { McpServerInfo } from "../McpServerInfo"; import type { Resource } from "../Resource"; import type { ResourceTemplate } from "../ResourceTemplate"; import type { Tool } from "../Tool"; import type { McpAuthStatus } from "./McpAuthStatus"; -export type McpServerStatus = { name: string, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; +export type McpServerStatus = { name: string, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array, resourceTemplates: Array, authStatus: McpAuthStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkUnixSocketPermission.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkUnixSocketPermission.ts index 466c6e5f8f..c5474cbb60 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkUnixSocketPermission.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkUnixSocketPermission.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type NetworkUnixSocketPermission = "allow" | "none"; +export type NetworkUnixSocketPermission = "allow" | "deny"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts new file mode 100644 index 0000000000..bcddb9f6f5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type SkillsExtraRootsSetParams = { extraRoots: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts new file mode 100644 index 0000000000..63be08186d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillsExtraRootsSetResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts index d917094e36..5fa30b64e3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Thread.ts @@ -17,6 +17,10 @@ sessionId: string, * Source thread id when this thread was created by forking another thread. */ forkedFromId: string | null, +/** + * The ID of the parent thread. This will only be set if this thread is a subagent. + */ +parentThreadId: string | null, /** * Usually the first user message in the thread, if available. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 1b655b460e..78dff0d765 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -23,7 +23,7 @@ import type { PatchApplyStatus } from "./PatchApplyStatus"; import type { UserInput } from "./UserInput"; import type { WebSearchAction } from "./WebSearchAction"; -export type ThreadItem = { "type": "userMessage", id: string, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, +export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array, } | { "type": "hookPrompt", id: string, fragments: Array, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array, content: Array, } | { "type": "commandExecution", id: string, /** * The command to be executed. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeInitialTurnsPageParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeInitialTurnsPageParams.ts new file mode 100644 index 0000000000..2dbcd97807 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeInitialTurnsPageParams.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SortDirection } from "./SortDirection"; +import type { TurnItemsView } from "./TurnItemsView"; + +export type ThreadResumeInitialTurnsPageParams = { +/** + * Optional turn page size. + */ +limit?: number | null, +/** + * Optional turn pagination direction; defaults to descending. + */ +sortDirection?: SortDirection | null, +/** + * How much item detail to include for each returned turn; defaults to summary. + */ +itemsView?: TurnItemsView | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index b04919d86b..afe1ac6d94 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -10,7 +10,7 @@ import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; -export type TurnStartParams = {threadId: string, input: Array, /** +export type TurnStartParams = {threadId: string, clientUserMessageId?: string | null, input: Array, /** * Override the working directory for this turn and subsequent turns. */ cwd?: string | null, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnSteerParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnSteerParams.ts index dae166b401..a984f2cba8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnSteerParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnSteerParams.ts @@ -3,7 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { UserInput } from "./UserInput"; -export type TurnSteerParams = {threadId: string, input: Array, /** +export type TurnSteerParams = {threadId: string, clientUserMessageId?: string | null, input: Array, /** * Required active turn id precondition. The request fails when it does not * match the currently active turn. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnsPage.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnsPage.ts new file mode 100644 index 0000000000..e91865aeeb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnsPage.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Turn } from "./Turn"; + +export type TurnsPage = { data: Array, nextCursor: string | null, backwardsCursor: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 5b4f2ed283..66b67fb1b4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -7,6 +7,8 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; export type { ActivePermissionProfile } from "./ActivePermissionProfile"; export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; +export type { AdditionalContextEntry } from "./AdditionalContextEntry"; +export type { AdditionalContextKind } from "./AdditionalContextKind"; export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; @@ -342,6 +344,8 @@ export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsChangedNotification } from "./SkillsChangedNotification"; export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse"; +export type { SkillsExtraRootsSetParams } from "./SkillsExtraRootsSetParams"; +export type { SkillsExtraRootsSetResponse } from "./SkillsExtraRootsSetResponse"; export type { SkillsListEntry } from "./SkillsListEntry"; export type { SkillsListParams } from "./SkillsListParams"; export type { SkillsListResponse } from "./SkillsListResponse"; @@ -396,6 +400,7 @@ export type { ThreadRealtimeStartTransport } from "./ThreadRealtimeStartTranspor export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification"; export type { ThreadRealtimeTranscriptDeltaNotification } from "./ThreadRealtimeTranscriptDeltaNotification"; export type { ThreadRealtimeTranscriptDoneNotification } from "./ThreadRealtimeTranscriptDoneNotification"; +export type { ThreadResumeInitialTurnsPageParams } from "./ThreadResumeInitialTurnsPageParams"; export type { ThreadResumeParams } from "./ThreadResumeParams"; export type { ThreadResumeResponse } from "./ThreadResumeResponse"; export type { ThreadRollbackParams } from "./ThreadRollbackParams"; @@ -448,6 +453,7 @@ export type { TurnStartedNotification } from "./TurnStartedNotification"; export type { TurnStatus } from "./TurnStatus"; export type { TurnSteerParams } from "./TurnSteerParams"; export type { TurnSteerResponse } from "./TurnSteerResponse"; +export type { TurnsPage } from "./TurnsPage"; export type { UserInput } from "./UserInput"; export type { WarningNotification } from "./WarningNotification"; export type { WebSearchAction } from "./WebSearchAction"; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index fcaa8a1eb5..56cf7038f2 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -948,10 +948,8 @@ impl ScanState { '(' => self.depth.paren += 1, ')' => self.depth.paren = (self.depth.paren - 1).max(0), '<' => self.depth.angle += 1, - '>' => { - if self.depth.angle > 0 { - self.depth.angle -= 1; - } + '>' if self.depth.angle > 0 => { + self.depth.angle -= 1; } _ => {} } @@ -2212,20 +2210,14 @@ mod tests { continue; } match ch { - '\\' => { - if in_single || in_double { - escape = true; - } + '\\' if (in_single || in_double) => { + escape = true; } - '\'' => { - if !in_double { - in_single = !in_single; - } + '\'' if !in_double => { + in_single = !in_single; } - '"' => { - if !in_single { - in_double = !in_double; - } + '"' if !in_single => { + in_double = !in_double; } '{' if !in_single && !in_double => level_brace += 1, '}' if !in_single && !in_double => level_brace -= 1, diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a8bec6b402..d532952f6c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -610,6 +610,11 @@ client_request_definitions! { serialization: global_shared_read("config"), response: v2::SkillsListResponse, }, + SkillsExtraRootsSet => "skills/extraRoots/set" { + params: v2::SkillsExtraRootsSetParams, + serialization: global("config"), + response: v2::SkillsExtraRootsSetResponse, + }, HooksList => "hooks/list" { params: v2::HooksListParams, serialization: global("config"), @@ -1721,6 +1726,17 @@ mod tests { Some(ClientRequestSerializationScope::GlobalSharedRead("config")) ); + let skills_extra_roots_set = ClientRequest::SkillsExtraRootsSet { + request_id: request_id(), + params: v2::SkillsExtraRootsSetParams { + extra_roots: vec![absolute_path("/tmp/skills")], + }, + }; + assert_eq!( + skills_extra_roots_set.serialization_scope(), + Some(ClientRequestSerializationScope::Global("config")) + ); + let plugin_list = ClientRequest::PluginList { request_id: request_id(), params: v2::PluginListParams { @@ -2312,6 +2328,7 @@ mod tests { id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(), session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(), forked_from_id: None, + parent_thread_id: None, preview: "first prompt".to_string(), ephemeral: true, model_provider: "openai".to_string(), @@ -2354,6 +2371,7 @@ mod tests { "id": "67e55044-10b1-426f-9247-bb680e5fe0c8", "sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7", "forkedFromId": null, + "parentThreadId": null, "preview": "first prompt", "ephemeral": true, "modelProvider": "openai", diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index c0c7d55528..aa65f9ab9d 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -278,7 +278,11 @@ impl ThreadHistoryBuilder { .unwrap_or_else(|| self.new_turn(/*id*/ None)); let id = self.next_item_id(); let content = self.build_user_inputs(payload); - turn.items.push(ThreadItem::UserMessage { id, content }); + turn.items.push(ThreadItem::UserMessage { + id, + client_id: payload.client_id.clone(), + content, + }); self.current_turn = Some(turn); } @@ -1246,6 +1250,7 @@ mod tests { fn builds_multiple_turns_with_reasoning_items() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "First turn".into(), images: Some(vec!["https://example.com/one.png".into()]), text_elements: Vec::new(), @@ -1264,6 +1269,7 @@ mod tests { text: "full reasoning".into(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Second turn".into(), images: None, text_elements: Vec::new(), @@ -1292,6 +1298,7 @@ mod tests { first.items[0], ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![ UserInput::Text { text: "First turn".into(), @@ -1330,6 +1337,7 @@ mod tests { second.items[0], ThreadItem::UserMessage { id: "item-4".into(), + client_id: None, content: vec![UserInput::Text { text: "Second turn".into(), text_elements: Vec::new(), @@ -1352,6 +1360,7 @@ mod tests { let local_path = PathBuf::from("/tmp/local.png"); let events = vec![RolloutItem::EventMsg(EventMsg::UserMessage( UserMessageEvent { + client_id: None, message: "inspect these".into(), images: Some(vec!["https://example.com/image.png".into()]), image_details: vec![Some(ImageDetail::Original)], @@ -1368,6 +1377,7 @@ mod tests { turns[0].items[0], ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![ UserInput::Text { text: "inspect these".into(), @@ -1399,6 +1409,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "hello".into(), images: None, text_elements: Vec::new(), @@ -1410,6 +1421,7 @@ mod tests { turn_id: turn_id.to_string(), item: CoreTurnItem::UserMessage(CoreUserMessageItem { id: "user-item-id".to_string(), + client_id: None, content: Vec::new(), }), started_at_ms: 0, @@ -1434,6 +1446,7 @@ mod tests { turns[0].items[0], ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1442,6 +1455,67 @@ mod tests { ); } + #[test] + fn preserves_user_message_client_id_from_legacy_event() { + let turn_id = "turn-1"; + let thread_id = ThreadId::new(); + let events = vec![ + EventMsg::TurnStarted(TurnStartedEvent { + turn_id: turn_id.to_string(), + trace_id: None, + started_at: None, + model_context_window: None, + collaboration_mode_kind: Default::default(), + }), + EventMsg::ItemStarted(ItemStartedEvent { + thread_id, + turn_id: turn_id.to_string(), + item: CoreTurnItem::UserMessage(CoreUserMessageItem { + id: "user-item-id".to_string(), + client_id: Some("client-message-1".to_string()), + content: vec![codex_protocol::user_input::UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + }), + started_at_ms: 0, + }), + EventMsg::UserMessage(UserMessageEvent { + client_id: Some("client-message-1".to_string()), + message: "hello".into(), + images: None, + text_elements: Vec::new(), + local_images: Vec::new(), + ..Default::default() + }), + EventMsg::TurnComplete(TurnCompleteEvent { + turn_id: turn_id.to_string(), + last_agent_message: None, + completed_at: None, + duration_ms: None, + time_to_first_token_ms: None, + }), + ]; + + let items = events + .into_iter() + .map(RolloutItem::EventMsg) + .collect::>(); + let turns = build_turns_from_rollout_items(&items); + assert_eq!(turns.len(), 1); + assert_eq!( + turns[0].items, + vec![ThreadItem::UserMessage { + id: "item-1".into(), + client_id: Some("client-message-1".to_string()), + content: vec![UserInput::Text { + text: "hello".into(), + text_elements: Vec::new(), + }], + }] + ); + } + #[test] fn preserves_agent_message_phase_in_history() { let events = vec![EventMsg::AgentMessage(AgentMessageEvent { @@ -1478,6 +1552,7 @@ mod tests { collaboration_mode_kind: Default::default(), })), RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "generate an image".into(), images: None, text_elements: Vec::new(), @@ -1515,6 +1590,7 @@ mod tests { items: vec![ ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "generate an image".into(), text_elements: Vec::new(), @@ -1536,6 +1612,7 @@ mod tests { fn splits_reasoning_when_interleaved() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Turn start".into(), images: None, text_elements: Vec::new(), @@ -1589,6 +1666,7 @@ mod tests { fn marks_turn_as_interrupted_when_aborted() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Please do the thing".into(), images: None, text_elements: Vec::new(), @@ -1607,6 +1685,7 @@ mod tests { duration_ms: None, }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Let's try again".into(), images: None, text_elements: Vec::new(), @@ -1634,6 +1713,7 @@ mod tests { first_turn.items[0], ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "Please do the thing".into(), text_elements: Vec::new(), @@ -1657,6 +1737,7 @@ mod tests { second_turn.items[0], ThreadItem::UserMessage { id: "item-3".into(), + client_id: None, content: vec![UserInput::Text { text: "Let's try again".into(), text_elements: Vec::new(), @@ -1678,6 +1759,7 @@ mod tests { fn drops_last_turns_on_thread_rollback() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "First".into(), images: None, text_elements: Vec::new(), @@ -1690,6 +1772,7 @@ mod tests { memory_citation: None, }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Second".into(), images: None, text_elements: Vec::new(), @@ -1703,6 +1786,7 @@ mod tests { }), EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Third".into(), images: None, text_elements: Vec::new(), @@ -1732,6 +1816,7 @@ mod tests { vec![ ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "First".into(), text_elements: Vec::new(), @@ -1750,6 +1835,7 @@ mod tests { vec![ ThreadItem::UserMessage { id: "item-3".into(), + client_id: None, content: vec![UserInput::Text { text: "Third".into(), text_elements: Vec::new(), @@ -1769,6 +1855,7 @@ mod tests { fn thread_rollback_clears_all_turns_when_num_turns_exceeds_history() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "One".into(), images: None, text_elements: Vec::new(), @@ -1781,6 +1868,7 @@ mod tests { memory_citation: None, }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Two".into(), images: None, text_elements: Vec::new(), @@ -1814,6 +1902,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Start".into(), images: None, text_elements: Vec::new(), @@ -1821,6 +1910,7 @@ mod tests { ..Default::default() }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "Steer".into(), images: None, text_elements: Vec::new(), @@ -1848,6 +1938,7 @@ mod tests { vec![ ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "Start".into(), text_elements: Vec::new(), @@ -1855,6 +1946,7 @@ mod tests { }, ThreadItem::UserMessage { id: "item-2".into(), + client_id: None, content: vec![UserInput::Text { text: "Steer".into(), text_elements: Vec::new(), @@ -1875,6 +1967,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "run tools".into(), images: None, text_elements: Vec::new(), @@ -2054,6 +2147,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "run dynamic tool".into(), images: None, text_elements: Vec::new(), @@ -2121,6 +2215,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "run tools".into(), images: None, text_elements: Vec::new(), @@ -2212,6 +2307,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "review this command".into(), images: None, text_elements: Vec::new(), @@ -2297,6 +2393,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "run a subcommand".into(), images: None, text_elements: Vec::new(), @@ -2362,6 +2459,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "first".into(), images: None, text_elements: Vec::new(), @@ -2383,6 +2481,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "second".into(), images: None, text_elements: Vec::new(), @@ -2458,6 +2557,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "first".into(), images: None, text_elements: Vec::new(), @@ -2479,6 +2579,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "second".into(), images: None, text_elements: Vec::new(), @@ -2528,6 +2629,7 @@ mod tests { turns[1].items[0], ThreadItem::UserMessage { id: "item-2".into(), + client_id: None, content: vec![UserInput::Text { text: "second".into(), text_elements: Vec::new(), @@ -2549,6 +2651,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "apply patch".into(), images: None, text_elements: Vec::new(), @@ -2584,6 +2687,7 @@ mod tests { vec![ ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -2615,6 +2719,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "apply patch".into(), images: None, text_elements: Vec::new(), @@ -2652,6 +2757,7 @@ mod tests { vec![ ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -2681,6 +2787,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "first".into(), images: None, text_elements: Vec::new(), @@ -2702,6 +2809,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "second".into(), images: None, text_elements: Vec::new(), @@ -2751,6 +2859,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "first".into(), images: None, text_elements: Vec::new(), @@ -2772,6 +2881,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "second".into(), images: None, text_elements: Vec::new(), @@ -2846,6 +2956,7 @@ mod tests { fn reconstructs_collab_resume_end_item() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "resume agent".into(), images: None, text_elements: Vec::new(), @@ -2904,6 +3015,7 @@ mod tests { .expect("valid receiver thread id"); let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "spawn agent".into(), images: None, text_elements: Vec::new(), @@ -2966,6 +3078,7 @@ mod tests { .expect("valid receiver thread id"); let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "redirect".into(), images: None, text_elements: Vec::new(), @@ -3030,6 +3143,7 @@ mod tests { fn rollback_failed_error_does_not_mark_turn_failed() { let events = vec![ EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "hello".into(), images: None, text_elements: Vec::new(), @@ -3068,6 +3182,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "hello".into(), images: None, text_elements: Vec::new(), @@ -3105,6 +3220,7 @@ mod tests { items_view: TurnItemsView::Full, items: vec![ThreadItem::UserMessage { id: "item-1".into(), + client_id: None, content: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -3125,6 +3241,7 @@ mod tests { collaboration_mode_kind: Default::default(), }), EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "hello".into(), images: None, text_elements: Vec::new(), @@ -3184,6 +3301,7 @@ mod tests { collaboration_mode_kind: Default::default(), })), RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "hello".into(), images: None, text_elements: Vec::new(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index fffe213811..473bb1e03c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -1,6 +1,7 @@ use super::ApprovalsReviewer; use super::AskForApproval; use super::SandboxMode; +use super::WindowsSandboxSetupMode; use super::shared::default_enabled; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::AutoCompactTokenLimitScope; @@ -42,6 +43,19 @@ pub enum ConfigLayerSource { file: AbsolutePathBuf, }, + /// Enterprise-managed config layer delivered by the cloud config bundle. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + EnterpriseManaged { + /// Stable identifier for the delivered layer. + id: String, + + /// Admin-facing name for the delivered layer. This is surfaced in + /// diagnostics so users know which cloud layer needs administrator + /// attention. + name: String, + }, + /// User config layer from $CODEX_HOME/config.toml. This layer is special /// in that it is expected to be: /// - writable by the user @@ -89,6 +103,7 @@ impl ConfigLayerSource { match self { ConfigLayerSource::Mdm { .. } => 0, ConfigLayerSource::System { .. } => 10, + ConfigLayerSource::EnterpriseManaged { .. } => 15, ConfigLayerSource::User { profile, .. } => { if profile.is_some() { 21 @@ -358,6 +373,7 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub allowed_windows_sandbox_implementations: Option>, pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, @@ -490,7 +506,7 @@ pub enum NetworkDomainPermission { #[ts(export_to = "v2/")] pub enum NetworkUnixSocketPermission { Allow, - None, + Deny, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs index 6e6c1ef7d4..b8f47276a6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs @@ -48,6 +48,7 @@ v2_enum_from_core!( SessionFlags, Plugin, CloudRequirements, + CloudManagedConfig, LegacyManagedConfigFile, LegacyManagedConfigMdm, Unknown, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 39e489d6aa..d68485565e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -212,7 +212,11 @@ impl CommandAction { pub enum ThreadItem { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - UserMessage { id: String, content: Vec }, + UserMessage { + id: String, + client_id: Option, + content: Vec, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] HookPrompt { @@ -776,6 +780,7 @@ impl From for ThreadItem { match value { CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage { id: user.id, + client_id: user.client_id, content: user.content.into_iter().map(UserInput::from).collect(), }, CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index 5f92e83059..ae61f12b2d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -2,6 +2,7 @@ use super::shared::v2_enum_from_core; use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; use codex_protocol::items::McpToolCallError as CoreMcpToolCallError; use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::mcp::Resource as McpResource; pub use codex_protocol::mcp::ResourceContent as McpResourceContent; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; @@ -53,6 +54,7 @@ pub enum McpServerStatusDetail { #[ts(export_to = "v2/")] pub struct McpServerStatus { pub name: String, + pub server_info: Option, pub tools: std::collections::HashMap, pub resources: Vec, pub resource_templates: Vec, @@ -690,6 +692,7 @@ impl From for rmcp::model::CreateElicitatio Self { action: value.action.into(), content: value.content, + meta: None, } } } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index 70d283994e..1b7e8ba8ff 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -35,6 +35,18 @@ pub struct SkillsListResponse { pub data: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsExtraRootsSetParams { + pub extra_roots: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsExtraRootsSetResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 8f6824f397..1e7558363d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -11,6 +11,7 @@ use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::items::WebSearchItem; use codex_protocol::mcp::CallToolResult; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; @@ -111,6 +112,86 @@ fn thread_turns_list_params_accepts_items_view() { assert_eq!(params.items_view, Some(TurnItemsView::NotLoaded)); } +#[test] +fn thread_resume_params_accept_turns_page_bootstrap() { + let params = serde_json::from_value::(json!({ + "threadId": "thr_123", + "initialTurnsPage": { + "limit": 25, + "sortDirection": "asc", + "itemsView": "full", + }, + })) + .expect("thread resume params should deserialize"); + + assert_eq!(params.thread_id, "thr_123"); + assert_eq!( + params.initial_turns_page, + Some(ThreadResumeInitialTurnsPageParams { + limit: Some(25), + sort_direction: Some(SortDirection::Asc), + items_view: Some(TurnItemsView::Full), + }) + ); +} + +#[test] +fn thread_resume_response_round_trips_initial_turns_page() { + let response = ThreadResumeResponse { + thread: Thread { + id: "thr_123".to_string(), + session_id: "thr_123".to_string(), + forked_from_id: None, + parent_thread_id: None, + preview: String::new(), + ephemeral: false, + model_provider: "openai".to_string(), + created_at: 1, + updated_at: 1, + status: ThreadStatus::Idle, + path: None, + cwd: absolute_path("tmp"), + cli_version: "0.0.0".to_string(), + source: SessionSource::Exec, + thread_source: None, + agent_nickname: None, + agent_role: None, + git_info: None, + name: None, + turns: Vec::new(), + }, + model: "gpt-5".to_string(), + model_provider: "openai".to_string(), + service_tier: None, + cwd: absolute_path("tmp"), + runtime_workspace_roots: Vec::new(), + instruction_sources: Vec::new(), + approval_policy: AskForApproval::OnFailure, + approvals_reviewer: ApprovalsReviewer::User, + sandbox: SandboxPolicy::DangerFullAccess, + active_permission_profile: None, + reasoning_effort: None, + initial_turns_page: Some(TurnsPage { + data: Vec::new(), + next_cursor: Some("cursor_next".to_string()), + backwards_cursor: Some("cursor_back".to_string()), + }), + }; + + let value = serde_json::to_value(&response).expect("serialize thread resume response"); + assert_eq!( + value.get("initialTurnsPage"), + Some(&json!({ + "data": [], + "nextCursor": "cursor_next", + "backwardsCursor": "cursor_back", + })) + ); + let decoded = serde_json::from_value::(value) + .expect("deserialize thread resume response"); + assert_eq!(decoded, response); +} + #[test] fn thread_turns_items_list_round_trips() { let params = ThreadTurnsItemsListParams { @@ -1590,6 +1671,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + allowed_windows_sandbox_implementations: None, allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, @@ -1676,6 +1758,7 @@ fn client_request_turn_start_granular_approval_policy_is_marked_experimental() { request_id: crate::RequestId::Integer(4), params: TurnStartParams { thread_id: "thr_123".to_string(), + client_user_message_id: None, input: Vec::new(), approval_policy: Some(AskForApproval::Granular { sandbox_approval: false, @@ -1699,6 +1782,7 @@ fn mcp_server_elicitation_response_round_trips_rmcp_result() { content: Some(json!({ "confirmed": true, })), + meta: None, }; let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone()); @@ -1923,6 +2007,80 @@ fn mcp_server_elicitation_response_serializes_nullable_content() { ); } +#[test] +fn mcp_server_status_serializes_absent_server_info_as_null() { + let response = ListMcpServerStatusResponse { + data: vec![McpServerStatus { + name: "not-ready".to_string(), + server_info: None, + tools: HashMap::new(), + resources: Vec::new(), + resource_templates: Vec::new(), + auth_status: McpAuthStatus::Unsupported, + }], + next_cursor: None, + }; + + assert_eq!( + serde_json::to_value(response).expect("response should serialize"), + json!({ + "data": [{ + "name": "not-ready", + "serverInfo": null, + "tools": {}, + "resources": [], + "resourceTemplates": [], + "authStatus": "unsupported", + }], + "nextCursor": null, + }) + ); +} + +#[test] +fn mcp_server_status_serializes_absent_server_info_metadata_as_null() { + let response = ListMcpServerStatusResponse { + data: vec![McpServerStatus { + name: "initialized".to_string(), + server_info: Some(McpServerInfo { + name: "lookup-server".to_string(), + title: None, + version: "1.0.0".to_string(), + description: None, + icons: None, + website_url: None, + }), + tools: HashMap::new(), + resources: Vec::new(), + resource_templates: Vec::new(), + auth_status: McpAuthStatus::Unsupported, + }], + next_cursor: None, + }; + + assert_eq!( + serde_json::to_value(response).expect("response should serialize"), + json!({ + "data": [{ + "name": "initialized", + "serverInfo": { + "name": "lookup-server", + "title": null, + "version": "1.0.0", + "description": null, + "icons": null, + "websiteUrl": null, + }, + "tools": {}, + "resources": [], + "resourceTemplates": [], + "authStatus": "unsupported", + }], + "nextCursor": null, + }) + ); +} + #[test] fn sandbox_policy_round_trips_workspace_write_access() { let v2_policy = SandboxPolicy::WorkspaceWrite { @@ -2122,7 +2280,7 @@ fn network_requirements_serializes_canonical_and_legacy_fields() { ), ( "/tmp/ignored.sock".to_string(), - NetworkUnixSocketPermission::None, + NetworkUnixSocketPermission::Deny, ), ])), allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), @@ -2146,7 +2304,7 @@ fn network_requirements_serializes_canonical_and_legacy_fields() { "allowedDomains": ["api.openai.com"], "deniedDomains": ["blocked.example.com"], "unixSockets": { - "/tmp/ignored.sock": "none", + "/tmp/ignored.sock": "deny", "/tmp/proxy.sock": "allow" }, "allowUnixSockets": ["/tmp/proxy.sock"], @@ -2159,6 +2317,7 @@ fn network_requirements_serializes_canonical_and_legacy_fields() { fn core_turn_item_into_thread_item_converts_supported_variants() { let user_item = TurnItem::UserMessage(UserMessageItem { id: "user-1".to_string(), + client_id: Some("client-message-1".to_string()), content: vec![ CoreUserInput::Text { text: "hello".to_string(), @@ -2187,6 +2346,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() { ThreadItem::from(user_item), ThreadItem::UserMessage { id: "user-1".to_string(), + client_id: Some("client-message-1".to_string()), content: vec![ UserInput::Text { text: "hello".to_string(), @@ -2468,6 +2628,27 @@ fn skills_list_params_serialization_uses_force_reload() { ); } +#[test] +fn skills_extra_roots_set_params_serialization_uses_extra_roots() { + assert_eq!( + serde_json::to_value(SkillsExtraRootsSetParams { + extra_roots: vec![absolute_path("tmp/skills")], + }) + .unwrap(), + json!({ + "extraRoots": [absolute_path_string("tmp/skills")], + }), + ); +} + +#[test] +fn skills_extra_roots_set_params_rejects_relative_roots() { + let result = serde_json::from_value::(json!({ + "extraRoots": ["relative/path"], + })); + assert!(result.is_err()); +} + #[test] fn plugin_source_serializes_local_git_and_remote_variants() { let local_path = if cfg!(windows) { @@ -3401,10 +3582,12 @@ fn thread_lifecycle_responses_default_missing_optional_fields() { let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response"); assert_eq!(start.instruction_sources, Vec::::new()); + assert_eq!(start.thread.parent_thread_id, None); assert_eq!(resume.instruction_sources, Vec::::new()); assert_eq!(fork.instruction_sources, Vec::::new()); assert_eq!(start.active_permission_profile, None); assert_eq!(resume.active_permission_profile, None); + assert_eq!(resume.initial_turns_page, None); assert_eq!(fork.active_permission_profile, None); } @@ -3426,8 +3609,10 @@ fn turn_start_params_preserve_explicit_null_service_tier() { let without_override = TurnStartParams { thread_id: "thread_123".to_string(), + client_user_message_id: None, input: vec![], responsesapi_client_metadata: None, + additional_context: None, environments: None, cwd: None, runtime_workspace_roots: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 4971d5f4b8..9dab60a046 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -164,12 +164,6 @@ pub struct ThreadStartParams { #[experimental("thread/start.experimentalRawEvents")] #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub experimental_raw_events: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/start.persistFullHistory")] - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub persist_extended_history: bool, } #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] @@ -397,12 +391,11 @@ pub struct ThreadResumeParams { #[experimental("thread/resume.excludeTurns")] #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub exclude_turns: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/resume.persistFullHistory")] - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub persist_extended_history: bool, + /// When present, include a `thread/turns/list` page in the resume response + /// so clients can bootstrap recent turns without a second request. + #[experimental("thread/resume.initialTurnsPage")] + #[ts(optional = nullable)] + pub initial_turns_page: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] @@ -435,6 +428,44 @@ pub struct ThreadResumeResponse { #[serde(default)] pub active_permission_profile: Option, pub reasoning_effort: Option, + /// `thread/turns/list` page returned when requested by `initialTurnsPage`. + #[experimental("thread/resume.initialTurnsPage")] + #[serde(default)] + pub initial_turns_page: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadResumeInitialTurnsPageParams { + /// Optional turn page size. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional turn pagination direction; defaults to descending. + #[ts(optional = nullable)] + pub sort_direction: Option, + /// How much item detail to include for each returned turn; defaults to summary. + #[ts(optional = nullable)] + pub items_view: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnsPage { + pub data: Vec, + pub next_cursor: Option, + pub backwards_cursor: Option, +} + +impl From for TurnsPage { + fn from(response: ThreadTurnsListResponse) -> Self { + Self { + data: response.data, + next_cursor: response.next_cursor, + backwards_cursor: response.backwards_cursor, + } + } } #[derive( @@ -514,12 +545,6 @@ pub struct ThreadForkParams { #[experimental("thread/fork.excludeTurns")] #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub exclude_turns: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/fork.persistFullHistory")] - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub persist_extended_history: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index f0c518adf8..35b6183871 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -108,6 +108,8 @@ pub struct Thread { pub session_id: String, /// Source thread id when this thread was created by forking another thread. pub forked_from_id: Option, + /// The ID of the parent thread. This will only be set if this thread is a subagent. + pub parent_thread_id: Option, /// Usually the first user message in the thread, if available. pub preview: String, /// Whether the thread is ephemeral and should not be materialized on disk. diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 50660af1d7..f9bfe21479 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -41,6 +41,23 @@ pub struct TurnEnvironmentParams { pub cwd: AbsolutePathBuf, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum AdditionalContextKind { + Untrusted, + Application, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalContextEntry { + pub value: String, + pub kind: AdditionalContextKind, +} + #[derive( Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, )] @@ -48,11 +65,17 @@ pub struct TurnEnvironmentParams { #[ts(export_to = "v2/")] pub struct TurnStartParams { pub thread_id: String, + #[ts(optional = nullable)] + pub client_user_message_id: Option, pub input: Vec, /// Optional turn-scoped Responses API client metadata. #[experimental("turn/start.responsesapiClientMetadata")] #[ts(optional = nullable)] pub responsesapi_client_metadata: Option>, + /// Optional client-provided context fragments keyed by an opaque source identifier. + #[experimental("turn/start.additionalContext")] + #[ts(optional = nullable)] + pub additional_context: Option>, /// Optional turn-scoped environments. /// /// Omitted uses the thread sticky environments. Empty disables @@ -136,11 +159,17 @@ pub struct TurnStartResponse { #[ts(export_to = "v2/")] pub struct TurnSteerParams { pub thread_id: String, + #[ts(optional = nullable)] + pub client_user_message_id: Option, pub input: Vec, /// Optional turn-scoped Responses API client metadata. #[experimental("turn/steer.responsesapiClientMetadata")] #[ts(optional = nullable)] pub responsesapi_client_metadata: Option>, + /// Optional client-provided context fragments keyed by an opaque source identifier. + #[experimental("turn/steer.additionalContext")] + #[ts(optional = nullable)] + pub additional_context: Option>, /// Required active turn id precondition. The request fails when it does not /// match the currently active turn. pub expected_turn_id: String, diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index 447fcf8cfe..18ed557ddf 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -196,7 +196,7 @@ fn canonicalize_json(value: &Value) -> Value { } Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize_json(child)); diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index fd9ecc845d..e89a8dbe99 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -734,6 +734,7 @@ async fn trigger_zsh_fork_multi_cmd_approval( let mut turn_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: message, text_elements: Vec::new(), @@ -818,6 +819,7 @@ async fn resume_message_v2( let turn_response = client.turn_start(TurnStartParams { thread_id: resume_response.thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: user_message, text_elements: Vec::new(), @@ -959,6 +961,7 @@ async fn send_message_v2_with_policies( println!("< thread/start response: {thread_response:?}"); let mut turn_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: user_message, // Test client sends plain text without UI element ranges. @@ -999,6 +1002,7 @@ async fn send_follow_up_v2( let first_turn_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: first_message, // Test client sends plain text without UI element ranges. @@ -1012,6 +1016,7 @@ async fn send_follow_up_v2( let follow_up_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: follow_up_message, // Test client sends plain text without UI element ranges. @@ -1255,6 +1260,7 @@ fn live_elicitation_timeout_pause( let started_at = Instant::now(); let turn_response = client.turn_start(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: prompt, text_elements: Vec::new(), diff --git a/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs b/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs index 4639942b08..22e5e74eda 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs @@ -15,13 +15,21 @@ use codex_app_server_protocol::JSONRPCMessage; use std::collections::HashMap; use tokio::sync::mpsc; use tokio::sync::watch; +use tokio::task::JoinHandle; use tokio::task::JoinSet; use tokio::time::Duration; use tokio::time::Instant; +use tokio::time::timeout; use tokio_util::sync::CancellationToken; +use tracing::info; +use tracing::warn; const REMOTE_CONTROL_CLIENT_IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60); pub(crate) const REMOTE_CONTROL_IDLE_SWEEP_INTERVAL: Duration = Duration::from_secs(30); +#[cfg(not(test))] +const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(test)] +const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_millis(10); #[derive(Debug)] pub(crate) struct Stopped; @@ -138,9 +146,6 @@ impl ClientTracker { if let Some(connection_id) = self.clients.get_mut(&client_key).map(|client| { client.last_activity_at = Instant::now(); - if let Some(seq_id) = seq_id { - client.last_inbound_seq_id = Some(seq_id); - } client.connection_id }) { self.send_transport_event(TransportEvent::IncomingMessage { @@ -148,6 +153,7 @@ impl ClientTracker { message, }) .await?; + self.record_inbound_message_delivery(&client_key, seq_id); return Ok(()); } @@ -177,23 +183,37 @@ impl ClientTracker { disconnect_token.clone(), )); self.clients.insert( - client_key, + client_key.clone(), ClientState { connection_id, disconnect_token, last_activity_at: Instant::now(), - last_inbound_seq_id: if is_legacy_stream_id { None } else { seq_id }, + last_inbound_seq_id: None, status_tx, }, ); if is_legacy_stream_id { self.legacy_stream_ids.insert(client_id.clone(), stream_id); } - self.send_transport_event(TransportEvent::IncomingMessage { - connection_id, - message, - }) - .await + if let Err(err) = self + .send_transport_event(TransportEvent::IncomingMessage { + connection_id, + message, + }) + .await + { + if let Some(client) = self.remove_client(&client_key) { + client.disconnect_token.cancel(); + // The initialize send already timed out on this queue; preserve close + // delivery without blocking reconnect on the same backpressure. + drop(self.spawn_connection_closed(client.connection_id)); + } + return Err(err); + } + if !is_legacy_stream_id { + self.record_inbound_message_delivery(&client_key, seq_id); + } + Ok(()) } ClientEvent::ClientMessageChunk { .. } | ClientEvent::Ack { .. } => Ok(()), ClientEvent::Ping => { @@ -290,16 +310,9 @@ impl ClientTracker { &mut self, client_key: &(ClientId, StreamId), ) -> Result<(), Stopped> { - let Some(client) = self.clients.remove(client_key) else { + let Some(client) = self.remove_client(client_key) else { return Ok(()); }; - if self - .legacy_stream_ids - .get(&client_key.0) - .is_some_and(|stream_id| stream_id == &client_key.1) - { - self.legacy_stream_ids.remove(&client_key.0); - } client.disconnect_token.cancel(); self.send_transport_event(TransportEvent::ConnectionClosed { connection_id: client.connection_id, @@ -307,11 +320,109 @@ impl ClientTracker { .await } + fn remove_client(&mut self, client_key: &(ClientId, StreamId)) -> Option { + let client = self.clients.remove(client_key)?; + if self + .legacy_stream_ids + .get(&client_key.0) + .is_some_and(|stream_id| stream_id == &client_key.1) + { + self.legacy_stream_ids.remove(&client_key.0); + } + Some(client) + } + async fn send_transport_event(&self, event: TransportEvent) -> Result<(), Stopped> { - self.transport_event_tx - .send(event) - .await - .map_err(|_| Stopped) + let event = match event { + TransportEvent::ConnectionClosed { connection_id } => { + return self.send_connection_closed(connection_id).await; + } + event => event, + }; + + let event_name = transport_event_name(&event); + match timeout( + REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT, + self.transport_event_tx.send(event), + ) + .await + { + Ok(Ok(())) => Ok(()), + Ok(Err(_)) => { + warn!( + transport_event = event_name, + "remote control transport event receiver dropped" + ); + Err(Stopped) + } + Err(_) => { + warn!( + transport_event = event_name, + timeout = ?REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT, + "timed out forwarding remote control transport event" + ); + Err(Stopped) + } + } + } + + fn record_inbound_message_delivery( + &mut self, + client_key: &(ClientId, StreamId), + seq_id: Option, + ) { + // Timed forwarding can fail, so only dedupe retries after app-server receives it. + if let Some(seq_id) = seq_id + && let Some(client) = self.clients.get_mut(client_key) + { + client.last_inbound_seq_id = Some(seq_id); + } + } + + async fn send_connection_closed(&self, connection_id: ConnectionId) -> Result<(), Stopped> { + // Worker shutdown can abort the caller; detach the cleanup event before awaiting it. + match self.spawn_connection_closed(connection_id).await { + Ok(result) => result, + Err(err) => { + warn!( + transport_event = "connection_closed", + ?err, + "remote control transport event forwarding task failed" + ); + Err(Stopped) + } + } + } + + fn spawn_connection_closed( + &self, + connection_id: ConnectionId, + ) -> JoinHandle> { + info!( + connection_id = ?connection_id, + "forwarding remote control connection closed transport event" + ); + let transport_event_tx = self.transport_event_tx.clone(); + tokio::spawn(async move { + transport_event_tx + .send(TransportEvent::ConnectionClosed { connection_id }) + .await + .map_err(|_| { + warn!( + transport_event = "connection_closed", + "remote control transport event receiver dropped" + ); + Stopped + }) + }) + } +} + +fn transport_event_name(event: &TransportEvent) -> &'static str { + match event { + TransportEvent::ConnectionOpened { .. } => "connection_opened", + TransportEvent::ConnectionClosed { .. } => "connection_closed", + TransportEvent::IncomingMessage { .. } => "incoming_message", } } @@ -370,6 +481,13 @@ mod tests { } } + fn initialized_notification() -> JSONRPCMessage { + JSONRPCMessage::Notification(codex_app_server_protocol::JSONRPCNotification { + method: "initialized".to_string(), + params: None, + }) + } + #[tokio::test] async fn cancelled_outbound_task_emits_connection_closed() { let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); @@ -487,6 +605,256 @@ mod tests { .expect("shutdown should not hang on blocked server forwarding"); } + #[tokio::test] + async fn non_close_transport_event_send_times_out_when_queue_stays_full() { + let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (transport_event_tx, _transport_event_rx) = mpsc::channel(1); + let shutdown_token = CancellationToken::new(); + let client_tracker = + ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token); + + transport_event_tx + .send(TransportEvent::ConnectionClosed { + connection_id: next_connection_id(), + }) + .await + .expect("transport event queue should accept prefill"); + + let send_result = client_tracker + .send_transport_event(TransportEvent::IncomingMessage { + connection_id: next_connection_id(), + message: initialized_notification(), + }) + .await; + + assert!(send_result.is_err()); + } + + #[tokio::test] + async fn incoming_message_timeout_does_not_advance_seq_id() { + let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2); + let shutdown_token = CancellationToken::new(); + let mut client_tracker = + ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token); + + client_tracker + .handle_message(initialize_envelope_with_stream_id( + "client-1", + Some("stream-1"), + )) + .await + .expect("initialize should open client"); + let connection_id = match transport_event_rx.recv().await.expect("open event") { + TransportEvent::ConnectionOpened { connection_id, .. } => connection_id, + other => panic!("expected connection opened, got {other:?}"), + }; + let _ = transport_event_rx.recv().await.expect("initialize event"); + + for _ in 0..2 { + transport_event_tx + .send(TransportEvent::ConnectionClosed { + connection_id: next_connection_id(), + }) + .await + .expect("transport event queue should accept prefill"); + } + + let retry_envelope = ClientEnvelope { + event: ClientEvent::ClientMessage { + message: initialized_notification(), + }, + client_id: ClientId("client-1".to_string()), + stream_id: Some(StreamId("stream-1".to_string())), + seq_id: Some(1), + cursor: None, + }; + assert!( + client_tracker + .handle_message(retry_envelope.clone()) + .await + .is_err() + ); + for _ in 0..2 { + let _ = transport_event_rx.recv().await.expect("prefilled event"); + } + + client_tracker + .handle_message(retry_envelope) + .await + .expect("retry should forward after timeout"); + match transport_event_rx.recv().await.expect("retried event") { + TransportEvent::IncomingMessage { + connection_id: queued_connection_id, + .. + } => assert_eq!(queued_connection_id, connection_id), + other => panic!("expected incoming message, got {other:?}"), + } + } + + #[tokio::test] + async fn initialize_timeout_closes_open_connection() { + let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (transport_event_tx, mut transport_event_rx) = mpsc::channel(1); + let shutdown_token = CancellationToken::new(); + let client_tracker = + ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token); + let mut handle_message = tokio::spawn(async move { + let mut client_tracker = client_tracker; + client_tracker + .handle_message(initialize_envelope_with_stream_id( + "client-1", + Some("stream-1"), + )) + .await + }); + + assert!( + timeout(Duration::from_millis(50), &mut handle_message) + .await + .expect("initialize timeout rollback should not wait for close delivery") + .expect("handle message task should not panic") + .is_err() + ); + let connection_id = match transport_event_rx.recv().await.expect("open event") { + TransportEvent::ConnectionOpened { connection_id, .. } => connection_id, + other => panic!("expected connection opened, got {other:?}"), + }; + + match transport_event_rx.recv().await.expect("close event") { + TransportEvent::ConnectionClosed { + connection_id: closed_connection_id, + } => assert_eq!(closed_connection_id, connection_id), + other => panic!("expected connection closed, got {other:?}"), + } + } + + #[tokio::test] + async fn close_client_waits_for_transport_event_queue_capacity() { + let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2); + let shutdown_token = CancellationToken::new(); + let mut client_tracker = + ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token); + + client_tracker + .handle_message(initialize_envelope_with_stream_id( + "client-1", + Some("stream-1"), + )) + .await + .expect("initialize should open client"); + let connection_id = match transport_event_rx.recv().await.expect("open event") { + TransportEvent::ConnectionOpened { connection_id, .. } => connection_id, + other => panic!("expected connection opened, got {other:?}"), + }; + let _ = transport_event_rx.recv().await.expect("initialize event"); + + for _ in 0..2 { + client_tracker + .transport_event_tx + .send(TransportEvent::IncomingMessage { + connection_id, + message: initialized_notification(), + }) + .await + .expect("transport event queue should accept prefill"); + } + + let client_key = ( + ClientId("client-1".to_string()), + StreamId("stream-1".to_string()), + ); + let close_client = client_tracker.close_client(&client_key); + tokio::pin!(close_client); + assert!( + timeout(Duration::from_millis(20), &mut close_client) + .await + .is_err() + ); + + for _ in 0..2 { + match transport_event_rx.recv().await.expect("prefilled event") { + TransportEvent::IncomingMessage { + connection_id: queued_connection_id, + .. + } => assert_eq!(queued_connection_id, connection_id), + other => panic!("expected incoming message, got {other:?}"), + } + } + + close_client + .await + .expect("close should forward after queue drains"); + match transport_event_rx.recv().await.expect("close event") { + TransportEvent::ConnectionClosed { + connection_id: closed_connection_id, + } => assert_eq!(closed_connection_id, connection_id), + other => panic!("expected connection closed, got {other:?}"), + } + } + + #[tokio::test] + async fn close_client_keeps_forwarding_after_caller_is_aborted() { + let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2); + let shutdown_token = CancellationToken::new(); + let mut client_tracker = + ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token); + + client_tracker + .handle_message(initialize_envelope_with_stream_id( + "client-1", + Some("stream-1"), + )) + .await + .expect("initialize should open client"); + let connection_id = match transport_event_rx.recv().await.expect("open event") { + TransportEvent::ConnectionOpened { connection_id, .. } => connection_id, + other => panic!("expected connection opened, got {other:?}"), + }; + let _ = transport_event_rx.recv().await.expect("initialize event"); + + for _ in 0..2 { + client_tracker + .transport_event_tx + .send(TransportEvent::IncomingMessage { + connection_id, + message: initialized_notification(), + }) + .await + .expect("transport event queue should accept prefill"); + } + + let client_key = ( + ClientId("client-1".to_string()), + StreamId("stream-1".to_string()), + ); + let mut close_client = + tokio::spawn(async move { client_tracker.close_client(&client_key).await }); + assert!( + timeout(Duration::from_millis(20), &mut close_client) + .await + .is_err() + ); + close_client.abort(); + let _ = close_client.await; + + for _ in 0..2 { + let _ = transport_event_rx.recv().await.expect("prefilled event"); + } + match timeout(Duration::from_secs(1), transport_event_rx.recv()) + .await + .expect("close should be delivered") + .expect("close event") + { + TransportEvent::ConnectionClosed { + connection_id: closed_connection_id, + } => assert_eq!(closed_connection_id, connection_id), + other => panic!("expected connection closed, got {other:?}"), + } + } + #[tokio::test] async fn initialize_with_new_stream_id_opens_new_connection_for_same_client() { let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY); diff --git a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs index 9ec6e4d004..6037caa651 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs @@ -1,18 +1,24 @@ use super::protocol::EnrollRemoteServerRequest; use super::protocol::EnrollRemoteServerResponse; +use super::protocol::RefreshRemoteServerRequest; use super::protocol::RemoteControlTarget; use axum::http::HeaderMap; use codex_api::SharedAuthProvider; use codex_login::default_client::build_reqwest_client; use codex_state::RemoteControlEnrollmentRecord; use codex_state::StateRuntime; +use serde::Serialize; +use serde::de::DeserializeOwned; use std::io; use std::io::ErrorKind; +use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; use tracing::info; use tracing::warn; const REMOTE_CONTROL_ENROLL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); const REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES: usize = 4096; +const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS: i64 = 30; const REQUEST_ID_HEADER: &str = "x-request-id"; const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id"; @@ -26,6 +32,24 @@ pub(super) struct RemoteControlEnrollment { pub(super) environment_id: String, pub(super) server_id: String, pub(super) server_name: String, + pub(super) remote_control_token: Option, + pub(super) expires_at: Option, +} + +impl RemoteControlEnrollment { + pub(super) fn should_refresh_server_token(&self) -> bool { + self.remote_control_token.is_none() + || self.expires_at.is_none_or(|expires_at| { + expires_at.unix_timestamp() + <= OffsetDateTime::now_utc().unix_timestamp() + + REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS + }) + } + + pub(super) fn clear_server_token(&mut self) { + self.remote_control_token = None; + self.expires_at = None; + } } pub(super) struct RemoteControlConnectionAuth { @@ -81,6 +105,8 @@ pub(super) async fn load_persisted_remote_control_enrollment( environment_id: enrollment.environment_id, server_id: enrollment.server_id, server_name: enrollment.server_name, + remote_control_token: None, + expires_at: None, })) } None => { @@ -164,19 +190,33 @@ pub(crate) fn preview_remote_control_response_body(body: &[u8]) -> String { if trimmed.is_empty() { return "".to_string(); } - if trimmed.len() <= REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES { - return trimmed.to_string(); + let redacted = redact_remote_control_response_body(trimmed); + if redacted.len() <= REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES { + return redacted; } let mut cut = REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES; - while !trimmed.is_char_boundary(cut) { + while !redacted.is_char_boundary(cut) { cut = cut.saturating_sub(1); } - let mut truncated = trimmed[..cut].to_string(); + let mut truncated = redacted[..cut].to_string(); truncated.push_str("..."); truncated } +fn redact_remote_control_response_body(body: &str) -> String { + let Ok(mut body_json) = serde_json::from_str::(body) else { + return body.to_string(); + }; + let Some(body_object) = body_json.as_object_mut() else { + return body.to_string(); + }; + if let Some(remote_control_token) = body_object.get_mut("remote_control_token") { + *remote_control_token = serde_json::Value::String("".to_string()); + } + body_json.to_string() +} + pub(crate) fn format_headers(headers: &HeaderMap) -> String { let request_id_str = headers .get(REQUEST_ID_HEADER) @@ -204,58 +244,146 @@ pub(super) async fn enroll_remote_control_server( app_server_version: env!("CARGO_PKG_VERSION"), installation_id: installation_id.to_string(), }; + let enrollment_response = send_remote_control_server_request::<_, EnrollRemoteServerResponse>( + enroll_url, + auth, + installation_id, + &request, + "enroll", + "server enrollment", + ) + .await?; + let mut enrollment = RemoteControlEnrollment { + account_id: auth.account_id.clone(), + environment_id: enrollment_response.environment_id, + server_id: enrollment_response.server_id, + server_name: server_name.to_string(), + remote_control_token: None, + expires_at: None, + }; + update_remote_control_server_token( + &mut enrollment, + enroll_url, + enrollment_response.remote_control_token, + enrollment_response.expires_at, + )?; + Ok(enrollment) +} + +pub(super) async fn refresh_remote_control_server( + remote_control_target: &RemoteControlTarget, + auth: &RemoteControlConnectionAuth, + installation_id: &str, + enrollment: &mut RemoteControlEnrollment, +) -> io::Result<()> { + let refresh_url = &remote_control_target.refresh_url; + let request = RefreshRemoteServerRequest { + server_id: enrollment.server_id.clone(), + installation_id: installation_id.to_string(), + }; + let refreshed = send_remote_control_server_request::<_, EnrollRemoteServerResponse>( + refresh_url, + auth, + installation_id, + &request, + "refresh", + "server refresh", + ) + .await?; + if refreshed.server_id != enrollment.server_id + || refreshed.environment_id != enrollment.environment_id + { + return Err(io::Error::other(format!( + "remote control server refresh returned mismatched enrollment: expected server_id={}, environment_id={}; got server_id={}, environment_id={}", + enrollment.server_id, + enrollment.environment_id, + refreshed.server_id, + refreshed.environment_id + ))); + } + + update_remote_control_server_token( + enrollment, + refresh_url, + refreshed.remote_control_token, + refreshed.expires_at, + ) +} + +async fn send_remote_control_server_request( + url: &str, + auth: &RemoteControlConnectionAuth, + installation_id: &str, + request: &Request, + action: &str, + response_kind: &str, +) -> io::Result +where + Request: Serialize, + Response: DeserializeOwned, +{ let client = build_reqwest_client(); let mut auth_headers = HeaderMap::new(); auth.auth_provider.add_auth_headers(&mut auth_headers); - let http_request = client - .post(enroll_url) + let response = client + .post(url) .timeout(REMOTE_CONTROL_ENROLL_TIMEOUT) .headers(auth_headers) .header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id) .header(REMOTE_CONTROL_INSTALLATION_ID_HEADER, installation_id) - .json(&request); - - let response = http_request.send().await.map_err(|err| { - io::Error::other(format!( - "failed to enroll remote control server at `{enroll_url}`: {err}" - )) - })?; + .json(request) + .send() + .await + .map_err(|err| { + io::Error::other(format!( + "failed to {action} remote control server at `{url}`: {err}" + )) + })?; let headers = response.headers().clone(); let status = response.status(); let body = response.bytes().await.map_err(|err| { io::Error::other(format!( - "failed to read remote control enrollment response from `{enroll_url}`: {err}" + "failed to read remote control {response_kind} response from `{url}`: {err}" )) })?; let body_preview = preview_remote_control_response_body(&body); if !status.is_success() { let headers_str = format_headers(&headers); - let error_kind = if matches!(status.as_u16(), 401 | 403) { - ErrorKind::PermissionDenied - } else { - ErrorKind::Other + let error_kind = match status.as_u16() { + 401 | 403 => ErrorKind::PermissionDenied, + 404 => ErrorKind::NotFound, + _ => ErrorKind::Other, }; return Err(io::Error::new( error_kind, format!( - "remote control server enrollment failed at `{enroll_url}`: HTTP {status}, {headers_str}, body: {body_preview}" + "remote control {response_kind} failed at `{url}`: HTTP {status}, {headers_str}, body: {body_preview}" ), )); } - let enrollment = serde_json::from_slice::(&body).map_err(|err| { + serde_json::from_slice::(&body).map_err(|err| { let headers_str = format_headers(&headers); io::Error::other(format!( - "failed to parse remote control enrollment response from `{enroll_url}`: HTTP {status}, {headers_str}, body: {body_preview}, decode error: {err}" + "failed to parse remote control {response_kind} response from `{url}`: HTTP {status}, {headers_str}, body: {body_preview}, decode error: {err}" + )) + }) +} + +fn update_remote_control_server_token( + enrollment: &mut RemoteControlEnrollment, + url: &str, + token: String, + expires_at: String, +) -> io::Result<()> { + let expires_at = OffsetDateTime::parse(&expires_at, &Rfc3339).map_err(|err| { + io::Error::other(format!( + "failed to parse remote control server token expiry from `{url}`: {err}" )) })?; - - Ok(RemoteControlEnrollment { - account_id: auth.account_id.clone(), - environment_id: enrollment.environment_id, - server_id: enrollment.server_id, - server_name: server_name.to_string(), - }) + enrollment.remote_control_token = Some(token); + enrollment.expires_at = Some(expires_at); + Ok(()) } #[cfg(test)] @@ -281,6 +409,40 @@ mod tests { .expect("state runtime should initialize") } + #[test] + fn remote_control_enrollment_refreshes_server_token_before_expiry() { + let expires_soon = RemoteControlEnrollment { + account_id: "account-a".to_string(), + environment_id: "env_first".to_string(), + server_id: "srv_e_first".to_string(), + server_name: "first-server".to_string(), + remote_control_token: Some("expires-soon".to_string()), + expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(29)), + }; + let expires_later = RemoteControlEnrollment { + expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(31)), + remote_control_token: Some("expires-later".to_string()), + ..expires_soon.clone() + }; + + assert!(expires_soon.should_refresh_server_token()); + assert!(!expires_later.should_refresh_server_token()); + } + + #[test] + fn preview_remote_control_response_body_redacts_server_token() { + assert_eq!( + serde_json::from_str::(&preview_remote_control_response_body( + br#"{"server_id":"srv_e_test","remote_control_token":"secret"}"# + )) + .expect("redacted response preview should stay valid json"), + json!({ + "server_id": "srv_e_test", + "remote_control_token": "", + }) + ); + } + #[tokio::test] async fn persisted_remote_control_enrollment_round_trips_by_target_and_account() { let codex_home = TempDir::new().expect("temp dir should create"); @@ -295,12 +457,16 @@ mod tests { environment_id: "env_first".to_string(), server_id: "srv_e_first".to_string(), server_name: "first-server".to_string(), + remote_control_token: None, + expires_at: None, }; let second_enrollment = RemoteControlEnrollment { account_id: "account-a".to_string(), environment_id: "env_second".to_string(), server_id: "srv_e_second".to_string(), server_name: "second-server".to_string(), + remote_control_token: None, + expires_at: None, }; update_persisted_remote_control_enrollment( @@ -371,12 +537,16 @@ mod tests { environment_id: "env_first".to_string(), server_id: "srv_e_first".to_string(), server_name: "first-server".to_string(), + remote_control_token: None, + expires_at: None, }; let second_enrollment = RemoteControlEnrollment { account_id: "account-a".to_string(), environment_id: "env_second".to_string(), server_id: "srv_e_second".to_string(), server_name: "second-server".to_string(), + remote_control_token: None, + expires_at: None, }; update_persisted_remote_control_enrollment( @@ -448,7 +618,8 @@ mod tests { normalize_remote_control_url(&remote_control_url).expect("target should parse"); let enroll_url = remote_control_target.enroll_url.clone(); let response_body = json!({ - "error": "not enrolled", + "server_id": "srv_e_test", + "environment_id": "env_test", }); let expected_body = response_body.to_string(); let server_task = tokio::spawn(async move { @@ -472,7 +643,7 @@ mod tests { assert_eq!( err.to_string(), format!( - "failed to parse remote control enrollment response from `{enroll_url}`: HTTP 200 OK, request-id: , cf-ray: , body: {expected_body}, decode error: missing field `server_id` at line 1 column {}", + "failed to parse remote control server enrollment response from `{enroll_url}`: HTTP 200 OK, request-id: , cf-ray: , body: {expected_body}, decode error: missing field `remote_control_token` at line 1 column {}", expected_body.len() ) ); diff --git a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs index ae6d79462c..357a98a30d 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs @@ -19,16 +19,20 @@ use codex_app_server_protocol::RemoteControlConnectionStatus; use codex_app_server_protocol::RemoteControlStatusChangedNotification; use codex_login::AuthManager; use codex_state::StateRuntime; +use futures::FutureExt; use gethostname::gethostname; use std::error::Error; use std::fmt; use std::io; +use std::panic::AssertUnwindSafe; use std::sync::Arc; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio::sync::watch; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; +use tracing::error; +use tracing::info; use tracing::warn; pub struct RemoteControlStartConfig { @@ -73,13 +77,21 @@ impl RemoteControlHandle { return Err(RemoteControlUnavailable); } - self.enabled_tx.send_if_modified(|state| { + let enabled_changed = self.enabled_tx.send_if_modified(|state| { let changed = !*state; *state = true; changed }); let status = self.status(); + info!( + enabled_changed, + current_status = ?status.status, + environment_id = ?status.environment_id, + installation_id = %status.installation_id, + server_name = %status.server_name, + "remote control enable requested" + ); if matches!( status.status, RemoteControlConnectionStatus::Connected | RemoteControlConnectionStatus::Connecting @@ -91,12 +103,21 @@ impl RemoteControlHandle { } pub fn disable(&self) -> RemoteControlStatusChangedNotification { - self.enabled_tx.send_if_modified(|state| { + let enabled_changed = self.enabled_tx.send_if_modified(|state| { let changed = *state; *state = false; changed }); + let status = self.status(); + info!( + enabled_changed, + current_status = ?status.status, + environment_id = ?status.environment_id, + installation_id = %status.installation_id, + server_name = %status.server_name, + "remote control disable requested" + ); self.publish_status(RemoteControlConnectionStatus::Disabled) } @@ -112,6 +133,7 @@ impl RemoteControlHandle { &self, connection_status: RemoteControlConnectionStatus, ) -> RemoteControlStatusChangedNotification { + let mut status_change = None; self.status_tx.send_if_modified(|status| { let next_status = remote_control_status_with_connection_status(status, connection_status); @@ -119,9 +141,21 @@ impl RemoteControlHandle { return false; } + status_change = Some((status.clone(), next_status.clone())); *status = next_status; true }); + if let Some((previous_status, next_status)) = status_change { + info!( + previous_status = ?previous_status.status, + next_status = ?next_status.status, + previous_environment_id = ?previous_status.environment_id, + next_environment_id = ?next_status.environment_id, + installation_id = %next_status.installation_id, + server_name = %next_status.server_name, + "remote control handle status changed" + ); + } self.status() } } @@ -165,6 +199,8 @@ pub async fn start_remote_control( let (enabled_tx, enabled_rx) = watch::channel(initial_enabled); let server_name = gethostname().to_string_lossy().trim().to_string(); + let remote_control_url = config.remote_control_url; + let installation_id = config.installation_id; let initial_status = RemoteControlStatusChangedNotification { status: if initial_enabled { RemoteControlConnectionStatus::Connecting @@ -172,16 +208,35 @@ pub async fn start_remote_control( RemoteControlConnectionStatus::Disabled }, server_name: server_name.clone(), - installation_id: config.installation_id.clone(), + installation_id: installation_id.clone(), environment_id: None, }; let (status_tx, _status_rx) = watch::channel(initial_status); let status_publisher = RemoteControlStatusPublisher::new(status_tx.clone()); + info!( + remote_control_url = %remote_control_url, + installation_id = %installation_id, + server_name = %server_name, + state_db_available, + initial_enabled, + "starting app-server remote control websocket task" + ); + let remote_control_url_for_log = remote_control_url.clone(); + let installation_id_for_log = installation_id.clone(); + let server_name_for_log = server_name.clone(); + let shutdown_token_for_log = shutdown_token.clone(); let join_handle = tokio::spawn(async move { - RemoteControlWebsocket::new( + info!( + remote_control_url = %remote_control_url_for_log, + installation_id = %installation_id_for_log, + server_name = %server_name_for_log, + initial_enabled, + "app-server remote control websocket task started" + ); + let websocket_task = RemoteControlWebsocket::new( websocket::RemoteControlWebsocketConfig { - remote_control_url: config.remote_control_url, - installation_id: config.installation_id, + remote_control_url, + installation_id, remote_control_target, server_name, }, @@ -194,8 +249,38 @@ pub async fn start_remote_control( shutdown_token, enabled_rx, ) - .run(app_server_client_name_rx) - .await; + .run(app_server_client_name_rx); + match AssertUnwindSafe(websocket_task).catch_unwind().await { + Ok(()) => { + let shutdown_requested = shutdown_token_for_log.is_cancelled(); + if shutdown_requested { + info!( + remote_control_url = %remote_control_url_for_log, + installation_id = %installation_id_for_log, + server_name = %server_name_for_log, + shutdown_requested, + "app-server remote control websocket task exited" + ); + } else { + warn!( + remote_control_url = %remote_control_url_for_log, + installation_id = %installation_id_for_log, + server_name = %server_name_for_log, + shutdown_requested, + "app-server remote control websocket task exited without shutdown" + ); + } + } + Err(panic) => { + error!( + remote_control_url = %remote_control_url_for_log, + installation_id = %installation_id_for_log, + server_name = %server_name_for_log, + "app-server remote control websocket task panicked" + ); + std::panic::resume_unwind(panic); + } + } }); Ok(( diff --git a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs index cee646b0d9..b10af42e86 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs @@ -11,6 +11,7 @@ use url::Url; pub(super) struct RemoteControlTarget { pub(super) websocket_url: String, pub(super) enroll_url: String, + pub(super) refresh_url: String, } #[derive(Debug, Serialize)] @@ -26,6 +27,14 @@ pub(super) struct EnrollRemoteServerRequest { pub(super) struct EnrollRemoteServerResponse { pub(super) server_id: String, pub(super) environment_id: String, + pub(super) remote_control_token: String, + pub(super) expires_at: String, +} + +#[derive(Debug, Serialize)] +pub(super) struct RefreshRemoteServerRequest { + pub(super) server_id: String, + pub(super) installation_id: String, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -177,6 +186,9 @@ pub(super) fn normalize_remote_control_url( let enroll_url = remote_control_url .join("wham/remote/control/server/enroll") .map_err(map_url_parse_error)?; + let refresh_url = remote_control_url + .join("wham/remote/control/server/refresh") + .map_err(map_url_parse_error)?; let mut websocket_url = remote_control_url .join("wham/remote/control/server") .map_err(map_url_parse_error)?; @@ -194,6 +206,7 @@ pub(super) fn normalize_remote_control_url( Ok(RemoteControlTarget { websocket_url: websocket_url.to_string(), enroll_url: enroll_url.to_string(), + refresh_url: refresh_url.to_string(), }) } @@ -212,6 +225,8 @@ mod tests { .to_string(), enroll_url: "https://chatgpt.com/backend-api/wham/remote/control/server/enroll" .to_string(), + refresh_url: "https://chatgpt.com/backend-api/wham/remote/control/server/refresh" + .to_string(), } ); assert_eq!( @@ -224,6 +239,9 @@ mod tests { enroll_url: "https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/enroll" .to_string(), + refresh_url: + "https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/refresh" + .to_string(), } ); } @@ -238,6 +256,8 @@ mod tests { .to_string(), enroll_url: "http://localhost:8080/backend-api/wham/remote/control/server/enroll" .to_string(), + refresh_url: "http://localhost:8080/backend-api/wham/remote/control/server/refresh" + .to_string(), } ); assert_eq!( @@ -248,6 +268,9 @@ mod tests { .to_string(), enroll_url: "https://localhost:8443/backend-api/wham/remote/control/server/enroll" .to_string(), + refresh_url: + "https://localhost:8443/backend-api/wham/remote/control/server/refresh" + .to_string(), } ); } diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs index 6279072713..85bcf09222 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs @@ -58,6 +58,9 @@ use tokio_tungstenite::tungstenite; use tokio_util::sync::CancellationToken; const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; +const TEST_REMOTE_CONTROL_SERVER_TOKEN: &str = "Remote Control Token"; +const TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN: &str = "Refreshed Remote Control Token"; +const TEST_REMOTE_CONTROL_SERVER_TOKEN_EXPIRES_AT: &str = "2999-01-01T00:00:00Z"; fn remote_control_auth_manager() -> Arc { auth_manager_from_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) @@ -125,6 +128,19 @@ fn test_server_name() -> String { gethostname().to_string_lossy().trim().to_string() } +fn remote_control_server_token_response( + server_id: &str, + environment_id: &str, + remote_control_token: &str, +) -> serde_json::Value { + json!({ + "server_id": server_id, + "environment_id": environment_id, + "remote_control_token": remote_control_token, + "expires_at": TEST_REMOTE_CONTROL_SERVER_TOKEN_EXPIRES_AT, + }) +} + async fn expect_remote_control_status( status_rx: &mut watch::Receiver, expected_status: Option, @@ -203,7 +219,11 @@ async fn remote_control_transport_manages_virtual_clients_and_routes_messages() ); respond_with_json( enroll_request.stream, - json!({ "server_id": "srv_e_test", "environment_id": "env_test" }), + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; let mut websocket = accept_remote_control_connection(&listener).await; @@ -483,17 +503,31 @@ async fn remote_control_transport_reconnects_after_disconnect() { ); respond_with_json( enroll_request.stream, - json!({ "server_id": "srv_e_test", "environment_id": "env_test" }), + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; - let mut first_websocket = accept_remote_control_connection(&listener).await; + let (first_handshake_request, mut first_websocket) = + accept_remote_control_backend_connection(&listener).await; + assert_eq!( + first_handshake_request.headers.get("authorization"), + Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}")) + ); first_websocket .close(None) .await .expect("first websocket should close"); drop(first_websocket); - let mut second_websocket = accept_remote_control_connection(&listener).await; + let (second_handshake_request, mut second_websocket) = + accept_remote_control_backend_connection(&listener).await; + assert_eq!( + second_handshake_request.headers.get("authorization"), + Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}")) + ); expect_remote_control_status( &mut status_rx, /*expected_status*/ None, @@ -537,6 +571,91 @@ async fn remote_control_transport_reconnects_after_disconnect() { let _ = remote_task.await; } +#[tokio::test] +async fn remote_control_transport_refreshes_server_token_after_websocket_unauthorized() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let remote_control_url = remote_control_url_for_listener(&listener); + let codex_home = TempDir::new().expect("temp dir should create"); + let (transport_event_tx, _transport_event_rx) = + mpsc::channel::(CHANNEL_CAPACITY); + let shutdown_token = CancellationToken::new(); + let (remote_task, remote_handle) = start_remote_control( + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, + Some(remote_control_state_runtime(&codex_home).await), + remote_control_auth_manager(), + transport_event_tx, + shutdown_token.clone(), + /*app_server_client_name_rx*/ None, + /*initial_enabled*/ true, + ) + .await + .expect("remote control should start"); + let mut status_rx = remote_handle.status_receiver(); + + let enroll_request = accept_http_request(&listener).await; + assert_eq!( + enroll_request.request_line, + "POST /backend-api/wham/remote/control/server/enroll HTTP/1.1" + ); + respond_with_json( + enroll_request.stream, + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; + + let websocket_request = accept_http_request(&listener).await; + assert_eq!( + websocket_request.request_line, + "GET /backend-api/wham/remote/control/server HTTP/1.1" + ); + assert_eq!( + websocket_request.headers.get("authorization"), + Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}")) + ); + respond_with_status(websocket_request.stream, "401 Unauthorized", "").await; + + let refresh_request = accept_http_request(&listener).await; + assert_eq!( + refresh_request.request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + respond_with_json( + refresh_request.stream, + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; + + let (handshake_request, _websocket) = accept_remote_control_backend_connection(&listener).await; + expect_remote_control_status( + &mut status_rx, + /*expected_status*/ None, + Some("env_test"), + ) + .await; + assert_eq!( + handshake_request.headers.get("authorization"), + Some(&format!( + "Bearer {TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN}" + )) + ); + + shutdown_token.cancel(); + let _ = remote_task.await; +} + #[tokio::test] async fn remote_control_start_allows_remote_control_invalid_url_when_disabled() { let (transport_event_tx, _transport_event_rx) = @@ -696,7 +815,11 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() { ); respond_with_json( enroll_request.stream, - json!({ "server_id": "srv_e_test", "environment_id": "env_test" }), + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; let mut first_websocket = accept_remote_control_connection(&listener).await; @@ -801,7 +924,11 @@ async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks() { let enroll_request = accept_http_request(&listener).await; respond_with_json( enroll_request.stream, - json!({ "server_id": "srv_e_test", "environment_id": "env_test" }), + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; let mut first_websocket = accept_remote_control_connection(&listener).await; @@ -1008,7 +1135,11 @@ async fn remote_control_http_mode_enrolls_before_connecting() { ); respond_with_json( enroll_request.stream, - json!({ "server_id": "srv_e_test", "environment_id": "env_test" }), + remote_control_server_token_response( + "srv_e_test", + "env_test", + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; @@ -1026,13 +1157,13 @@ async fn remote_control_http_mode_enrolls_before_connecting() { ); assert_eq!( handshake_request.headers.get("authorization"), - Some(&"Bearer Access Token".to_string()) + Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}")) ); assert_eq!( handshake_request .headers .get(REMOTE_CONTROL_ACCOUNT_ID_HEADER), - Some(&"account_id".to_string()) + None ); assert_eq!( handshake_request @@ -1172,7 +1303,7 @@ async fn remote_control_http_mode_enrolls_before_connecting() { } #[tokio::test] -async fn remote_control_http_mode_reuses_persisted_enrollment_before_reenrolling() { +async fn remote_control_http_mode_refreshes_persisted_enrollment_before_connecting() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); @@ -1186,6 +1317,8 @@ async fn remote_control_http_mode_reuses_persisted_enrollment_before_reenrolling environment_id: "env_persisted".to_string(), server_id: "srv_e_persisted".to_string(), server_name: "persisted-server".to_string(), + remote_control_token: None, + expires_at: None, }; update_persisted_remote_control_enrollment( Some(state_db.as_ref()), @@ -1215,6 +1348,33 @@ async fn remote_control_http_mode_reuses_persisted_enrollment_before_reenrolling .await .expect("remote control should start"); + let refresh_request = accept_http_request(&listener).await; + assert_eq!( + refresh_request.request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + assert_eq!( + refresh_request.headers.get("authorization"), + Some(&"Bearer Access Token".to_string()) + ); + assert_eq!( + serde_json::from_str::(&refresh_request.body) + .expect("refresh body should deserialize"), + json!({ + "server_id": persisted_enrollment.server_id.clone(), + "installation_id": TEST_INSTALLATION_ID, + }) + ); + respond_with_json( + refresh_request.stream, + remote_control_server_token_response( + &persisted_enrollment.server_id, + &persisted_enrollment.environment_id, + TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; + let (handshake_request, _websocket) = accept_remote_control_backend_connection(&listener).await; assert_eq!( handshake_request.path, @@ -1224,6 +1384,12 @@ async fn remote_control_http_mode_reuses_persisted_enrollment_before_reenrolling handshake_request.headers.get("x-codex-server-id"), Some(&persisted_enrollment.server_id) ); + assert_eq!( + handshake_request.headers.get("authorization"), + Some(&format!( + "Bearer {TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN}" + )) + ); assert_eq!( load_persisted_remote_control_enrollment( Some(state_db.as_ref()), @@ -1256,6 +1422,8 @@ async fn remote_control_stdio_mode_waits_for_client_name_before_connecting() { environment_id: "env_persisted".to_string(), server_id: "srv_e_persisted".to_string(), server_name: "persisted-server".to_string(), + remote_control_token: None, + expires_at: None, }; update_persisted_remote_control_enrollment( Some(state_db.as_ref()), @@ -1291,6 +1459,20 @@ async fn remote_control_stdio_mode_waits_for_client_name_before_connecting() { .expect_err("remote control should wait for the stdio client name"); let _ = app_server_client_name_tx.send(app_server_client_name.to_string()); + let refresh_request = accept_http_request(&listener).await; + assert_eq!( + refresh_request.request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + respond_with_json( + refresh_request.stream, + remote_control_server_token_response( + &persisted_enrollment.server_id, + &persisted_enrollment.environment_id, + TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; let (handshake_request, _websocket) = accept_remote_control_backend_connection(&listener).await; assert_eq!( handshake_request.headers.get("x-codex-server-id"), @@ -1328,6 +1510,8 @@ async fn remote_control_waits_for_account_id_before_enrolling() { environment_id: "env_ready".to_string(), server_id: "srv_e_ready".to_string(), server_name: expected_server_name, + remote_control_token: None, + expires_at: None, }; let (transport_event_tx, _transport_event_rx) = @@ -1369,10 +1553,11 @@ async fn remote_control_waits_for_account_id_before_enrolling() { ); respond_with_json( enroll_request.stream, - json!({ - "server_id": expected_enrollment.server_id, - "environment_id": expected_enrollment.environment_id, - }), + remote_control_server_token_response( + &expected_enrollment.server_id, + &expected_enrollment.environment_id, + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; @@ -1387,7 +1572,7 @@ async fn remote_control_waits_for_account_id_before_enrolling() { } #[tokio::test] -async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() { +async fn remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); @@ -1402,12 +1587,16 @@ async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() environment_id: "env_stale".to_string(), server_id: "srv_e_stale".to_string(), server_name: "stale-server".to_string(), + remote_control_token: None, + expires_at: None, }; let refreshed_enrollment = RemoteControlEnrollment { account_id: "account_id".to_string(), environment_id: "env_refreshed".to_string(), server_id: "srv_e_refreshed".to_string(), server_name: expected_server_name, + remote_control_token: None, + expires_at: None, }; update_persisted_remote_control_enrollment( Some(state_db.as_ref()), @@ -1438,6 +1627,138 @@ async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() .expect("remote control should start"); let mut status_rx = remote_handle.status_receiver(); + let refresh_request = accept_http_request(&listener).await; + assert_eq!( + refresh_request.request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + expect_remote_control_status( + &mut status_rx, + /*expected_status*/ None, + Some("env_stale"), + ) + .await; + respond_with_status(refresh_request.stream, "404 Not Found", "").await; + expect_remote_control_status( + &mut status_rx, + /*expected_status*/ None, + /*expected_environment_id*/ None, + ) + .await; + + let enroll_request = accept_http_request(&listener).await; + assert_eq!( + enroll_request.request_line, + "POST /backend-api/wham/remote/control/server/enroll HTTP/1.1" + ); + respond_with_json( + enroll_request.stream, + remote_control_server_token_response( + &refreshed_enrollment.server_id, + &refreshed_enrollment.environment_id, + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; + + let (handshake_request, _websocket) = accept_remote_control_backend_connection(&listener).await; + expect_remote_control_status( + &mut status_rx, + /*expected_status*/ None, + Some("env_refreshed"), + ) + .await; + assert_eq!( + handshake_request.headers.get("x-codex-server-id"), + Some(&refreshed_enrollment.server_id) + ); + assert_eq!( + load_persisted_remote_control_enrollment( + Some(state_db.as_ref()), + &remote_control_target, + "account_id", + /*app_server_client_name*/ None, + ) + .await + .expect("refreshed enrollment should load"), + Some(refreshed_enrollment) + ); + + shutdown_token.cancel(); + let _ = remote_task.await; +} + +#[tokio::test] +async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let remote_control_url = remote_control_url_for_listener(&listener); + let codex_home = TempDir::new().expect("temp dir should create"); + let state_db = remote_control_state_runtime(&codex_home).await; + let remote_control_target = + normalize_remote_control_url(&remote_control_url).expect("target should parse"); + let expected_server_name = gethostname().to_string_lossy().trim().to_string(); + let stale_enrollment = RemoteControlEnrollment { + account_id: "account_id".to_string(), + environment_id: "env_stale".to_string(), + server_id: "srv_e_stale".to_string(), + server_name: "stale-server".to_string(), + remote_control_token: None, + expires_at: None, + }; + let refreshed_enrollment = RemoteControlEnrollment { + account_id: "account_id".to_string(), + environment_id: "env_refreshed".to_string(), + server_id: "srv_e_refreshed".to_string(), + server_name: expected_server_name, + remote_control_token: None, + expires_at: None, + }; + update_persisted_remote_control_enrollment( + Some(state_db.as_ref()), + &remote_control_target, + "account_id", + /*app_server_client_name*/ None, + Some(&stale_enrollment), + ) + .await + .expect("stale enrollment should save"); + + let (transport_event_tx, _transport_event_rx) = + mpsc::channel::(CHANNEL_CAPACITY); + let shutdown_token = CancellationToken::new(); + let (remote_task, remote_handle) = start_remote_control( + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, + Some(state_db.clone()), + remote_control_auth_manager_with_home(&codex_home), + transport_event_tx, + shutdown_token.clone(), + /*app_server_client_name_rx*/ None, + /*initial_enabled*/ true, + ) + .await + .expect("remote control should start"); + let mut status_rx = remote_handle.status_receiver(); + + let refresh_request = accept_http_request(&listener).await; + assert_eq!( + refresh_request.request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + respond_with_json( + refresh_request.stream, + remote_control_server_token_response( + &stale_enrollment.server_id, + &stale_enrollment.environment_id, + TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN, + ), + ) + .await; + let websocket_request = accept_http_request(&listener).await; assert_eq!( websocket_request.request_line, @@ -1468,10 +1789,11 @@ async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() ); respond_with_json( enroll_request.stream, - json!({ - "server_id": refreshed_enrollment.server_id, - "environment_id": refreshed_enrollment.environment_id, - }), + remote_control_server_token_response( + &refreshed_enrollment.server_id, + &refreshed_enrollment.environment_id, + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ), ) .await; diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs index 1e696c2e46..21710b5e26 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs @@ -7,6 +7,7 @@ use crate::transport::remote_control::enroll::enroll_remote_control_server; use crate::transport::remote_control::enroll::format_headers; use crate::transport::remote_control::enroll::load_persisted_remote_control_enrollment; use crate::transport::remote_control::enroll::preview_remote_control_response_body; +use crate::transport::remote_control::enroll::refresh_remote_control_server; use crate::transport::remote_control::enroll::update_persisted_remote_control_enrollment; use super::protocol::ClientEnvelope; @@ -55,7 +56,6 @@ use tracing::info; use tracing::warn; pub(super) const REMOTE_CONTROL_PROTOCOL_VERSION: &str = "3"; -pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id"; pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id"; const REMOTE_CONTROL_SUBSCRIBE_CURSOR_HEADER: &str = "x-codex-subscribe-cursor"; const REMOTE_CONTROL_WEBSOCKET_PING_INTERVAL: std::time::Duration = @@ -66,6 +66,10 @@ const REMOTE_CONTROL_ACCOUNT_ID_RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); const REMOTE_CONTROL_RECONNECT_BACKOFF_CAP: std::time::Duration = std::time::Duration::from_secs(30); +const REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT: std::time::Duration = + std::time::Duration::from_secs(30); +const REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT: std::time::Duration = + std::time::Duration::from_secs(5); struct BoundedOutboundBuffer { buffer_by_stream: HashMap<(ClientId, StreamId), VecDeque>, @@ -179,14 +183,32 @@ impl WebsocketState { return ClientSegmentObservation::Dropped; } - let observation = self.client_segment_reassembler.observe(client_envelope); - if matches!(observation, ClientSegmentObservation::Forward(_)) - && let Some((key, seq_id)) = client_message_key - { + self.client_segment_reassembler.observe(client_envelope) + } + + fn record_client_message_delivery( + &mut self, + client_envelope: &ClientEnvelope, + client_message_key: Option<((ClientId, Option), u64)>, + ) { + if let Some(cursor) = client_envelope.cursor.as_deref() { + self.subscribe_cursor = Some(cursor.to_string()); + } + if let Some((key, seq_id)) = client_message_key { self.last_completed_client_chunk_seq_id_by_stream .insert(key, seq_id); } - observation + if let ClientEvent::Ack { segment_id } = &client_envelope.event + && let Some(acked_seq_id) = client_envelope.seq_id + && let Some(stream_id) = client_envelope.stream_id.as_ref() + { + self.outbound_buffer.ack( + &client_envelope.client_id, + stream_id, + acked_seq_id, + *segment_id, + ); + } } fn invalidate_client_message_stream(&mut self, client_id: &ClientId, stream_id: &StreamId) { @@ -255,6 +277,14 @@ enum ConnectOutcome { Shutdown, } +#[derive(Debug, Clone, Copy)] +enum ConnectionEndReason { + Shutdown, + Disabled, + EnabledWatchClosed, + ConnectionWorkerStopped, +} + pub(super) struct RemoteControlChannels { pub(super) transport_event_tx: mpsc::Sender, pub(super) status_publisher: RemoteControlStatusPublisher, @@ -270,7 +300,12 @@ impl RemoteControlStatusPublisher { Self { tx } } + fn status(&self) -> RemoteControlStatusChangedNotification { + self.tx.borrow().clone() + } + fn publish_status(&self, connection_status: RemoteControlConnectionStatus) { + let mut status_change = None; self.tx.send_if_modified(|status| { let next_status = remote_control_status_with_connection_status(status, connection_status); @@ -278,12 +313,25 @@ impl RemoteControlStatusPublisher { return false; } + status_change = Some((status.clone(), next_status.clone())); *status = next_status; true }); + if let Some((previous_status, next_status)) = status_change { + info!( + previous_status = ?previous_status.status, + next_status = ?next_status.status, + previous_environment_id = ?previous_status.environment_id, + next_environment_id = ?next_status.environment_id, + installation_id = %next_status.installation_id, + server_name = %next_status.server_name, + "remote control websocket status changed" + ); + } } fn publish_environment_id(&self, environment_id: Option) { + let mut status_change = None; self.tx.send_if_modified(|status| { if status.status == RemoteControlConnectionStatus::Disabled { return false; @@ -298,9 +346,20 @@ impl RemoteControlStatusPublisher { return false; } + status_change = Some((status.clone(), next_status.clone())); *status = next_status; true }); + if let Some((previous_status, next_status)) = status_change { + info!( + status = ?next_status.status, + previous_environment_id = ?previous_status.environment_id, + next_environment_id = ?next_status.environment_id, + installation_id = %next_status.installation_id, + server_name = %next_status.server_name, + "remote control websocket environment changed" + ); + } } } @@ -367,12 +426,26 @@ impl RemoteControlWebsocket { mut self, app_server_client_name_rx: Option>, ) { + info!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + initial_enabled = *self.enabled_rx.borrow(), + "app-server remote control websocket loop started" + ); let app_server_client_name = match self .wait_for_app_server_client_name(app_server_client_name_rx) .await { Ok(app_server_client_name) => app_server_client_name, Err(_) => { + warn!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + shutdown_requested = self.shutdown_token.is_cancelled(), + "app-server remote control websocket loop stopped before client name was ready" + ); self.client_tracker.lock().await.shutdown().await; return; } @@ -380,9 +453,27 @@ impl RemoteControlWebsocket { loop { if !self.wait_until_enabled().await { + info!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + shutdown_requested = self.shutdown_token.is_cancelled(), + current_status = ?self.status_publisher.status().status, + "app-server remote control websocket loop exiting while waiting for enablement" + ); break; } + let status = self.status_publisher.status(); + info!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + reconnect_attempt = self.reconnect_attempt.saturating_add(1), + current_status = ?status.status, + environment_id = ?status.environment_id, + "starting app-server remote control websocket connection cycle" + ); let shutdown_token = self.shutdown_token.child_token(); let websocket_connection = match self .connect(&shutdown_token, app_server_client_name.as_deref()) @@ -397,11 +488,30 @@ impl RemoteControlWebsocket { ConnectOutcome::Shutdown => break, }; - self.run_connection(websocket_connection, shutdown_token) + let connection_end_reason = self + .run_connection(websocket_connection, shutdown_token) .await; + let status = self.status_publisher.status(); + info!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + connection_end_reason = ?connection_end_reason, + current_status = ?status.status, + environment_id = ?status.environment_id, + enabled = *self.enabled_rx.borrow(), + "app-server remote control websocket connection cycle ended" + ); } self.client_tracker.lock().await.shutdown().await; + info!( + remote_control_url = %self.remote_control_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + shutdown_requested = self.shutdown_token.is_cancelled(), + "app-server remote control websocket loop exited" + ); } async fn wait_for_app_server_client_name( @@ -462,6 +572,19 @@ impl RemoteControlWebsocket { loop { let subscribe_cursor = self.state.lock().await.subscribe_cursor.clone(); + let enrollment = self.enrollment.as_ref(); + info!( + websocket_url = %remote_control_target.websocket_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + reconnect_attempt = self.reconnect_attempt.saturating_add(1), + has_enrollment = enrollment.is_some(), + server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()), + environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()), + subscribe_cursor_present = subscribe_cursor.is_some(), + app_server_client_name = ?app_server_client_name, + "connecting to app-server remote control websocket" + ); let connect_options = RemoteControlConnectOptions { installation_id: &self.installation_id, server_name: &self.server_name, @@ -500,10 +623,16 @@ impl RemoteControlWebsocket { self.auth_recovery = self.auth_manager.unauthorized_recovery(); self.status_publisher .publish_status(RemoteControlConnectionStatus::Connected); + let enrollment = self.enrollment.as_ref(); info!( - "connected to app-server remote control websocket: {}, {}", - remote_control_target.websocket_url, - format_headers(response.headers()) + websocket_url = %remote_control_target.websocket_url, + installation_id = %self.installation_id, + server_name = %self.server_name, + server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()), + environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()), + subscribe_cursor_present = subscribe_cursor.is_some(), + response_headers = %format_headers(response.headers()), + "connected to app-server remote control websocket" ); return ConnectOutcome::Connected(Box::new(websocket_connection)); } @@ -519,12 +648,20 @@ impl RemoteControlWebsocket { let reconnect_attempt = self.reconnect_attempt.saturating_add(1); let (reconnect_delay, reconnect_backoff_reset) = next_reconnect_delay(&mut self.reconnect_attempt); + let enrollment = self.enrollment.as_ref(); warn!( websocket_url = %remote_control_target.websocket_url, + installation_id = %self.installation_id, + server_name = %self.server_name, error = %err, + error_kind = ?err.kind(), reconnect_attempt, reconnect_delay = ?reconnect_delay, reconnect_backoff_reset, + has_enrollment = enrollment.is_some(), + server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()), + environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()), + subscribe_cursor_present = subscribe_cursor.is_some(), "failed to connect to app-server remote control websocket" ); if reconnect_backoff_reset { @@ -562,7 +699,7 @@ impl RemoteControlWebsocket { &self, websocket_connection: WebSocketStream>, shutdown_token: CancellationToken, - ) { + ) -> ConnectionEndReason { let (websocket_writer, websocket_reader) = websocket_connection.split(); let mut join_set = tokio::task::JoinSet::new(); @@ -583,19 +720,48 @@ impl RemoteControlWebsocket { )); let mut enabled_rx = self.enabled_rx.clone(); - tokio::select! { - _ = shutdown_token.cancelled() => {} + let connection_end_reason = tokio::select! { + _ = shutdown_token.cancelled() => ConnectionEndReason::Shutdown, changed = enabled_rx.wait_for(|enabled| !*enabled) => { if changed.is_ok() { self.status_publisher .publish_status(RemoteControlConnectionStatus::Disabled); + ConnectionEndReason::Disabled + } else { + ConnectionEndReason::EnabledWatchClosed } } - _ = join_set.join_next() => {} + _ = join_set.join_next() => ConnectionEndReason::ConnectionWorkerStopped, }; shutdown_token.cancel(); - join_set.join_all().await; + Self::join_connection_workers(&mut join_set, REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT) + .await; + connection_end_reason + } + + async fn join_connection_workers( + join_set: &mut tokio::task::JoinSet<()>, + shutdown_timeout: std::time::Duration, + ) { + if tokio::time::timeout(shutdown_timeout, Self::drain_join_set(join_set)) + .await + .is_ok() + { + return; + } + + warn!( + shutdown_timeout = ?shutdown_timeout, + remaining_workers = join_set.len(), + "timed out waiting for remote control connection workers to stop; aborting" + ); + join_set.abort_all(); + Self::drain_join_set(join_set).await; + } + + async fn drain_join_set(join_set: &mut tokio::task::JoinSet<()>) { + while join_set.join_next().await.is_some() {} } async fn run_server_writer( @@ -675,12 +841,14 @@ impl RemoteControlWebsocket { let queued_server_envelope = tokio::select! { _ = shutdown_token.cancelled() => return Ok(()), _ = ping_interval.tick() => { - if let Err(err) = websocket_writer - .send(tungstenite::Message::Ping(Vec::new().into())) - .await - { - return Err(io::Error::other(err)); - } + tokio::select! { + _ = shutdown_token.cancelled() => return Ok(()), + send_result = websocket_writer.send(tungstenite::Message::Ping(Vec::new().into())) => { + if let Err(err) = send_result { + return Err(io::Error::other(err)); + } + } + }; continue; } wait_result = used_rx.changed(), if !outbound_has_capacity => @@ -886,6 +1054,7 @@ impl RemoteControlWebsocket { } }; + let client_message_key = WebsocketState::client_message_key(&client_envelope); let observation = { let mut websocket_state = state.lock().await; websocket_state.observe_client_message(client_envelope, wire_size_bytes) @@ -895,24 +1064,6 @@ impl RemoteControlWebsocket { ClientSegmentObservation::Pending | ClientSegmentObservation::Dropped => continue, }; - { - let mut websocket_state = state.lock().await; - if let Some(cursor) = client_envelope.cursor.as_deref() { - websocket_state.subscribe_cursor = Some(cursor.to_string()); - } - if let ClientEvent::Ack { segment_id } = &client_envelope.event - && let Some(acked_seq_id) = client_envelope.seq_id - && let Some(stream_id) = client_envelope.stream_id.as_ref() - { - websocket_state.outbound_buffer.ack( - &client_envelope.client_id, - stream_id, - acked_seq_id, - *segment_id, - ); - } - } - let closed_client = matches!(&client_envelope.event, ClientEvent::ClientClosed).then(|| { ( @@ -920,6 +1071,7 @@ impl RemoteControlWebsocket { client_envelope.stream_id.clone(), ) }); + let delivered_client_envelope = client_envelope.clone(); if client_tracker .handle_message(client_envelope) .await @@ -927,6 +1079,10 @@ impl RemoteControlWebsocket { { return Ok(()); } + state + .lock() + .await + .record_client_message_delivery(&delivered_client_envelope, client_message_key); if let Some((client_id, stream_id)) = closed_client { let mut websocket_state = state.lock().await; if let Some(stream_id) = stream_id { @@ -963,7 +1119,6 @@ fn set_remote_control_header( fn build_remote_control_websocket_request( websocket_url: &str, enrollment: &RemoteControlEnrollment, - auth: &RemoteControlConnectionAuth, installation_id: &str, subscribe_cursor: Option<&str>, ) -> io::Result> { @@ -985,10 +1140,17 @@ fn build_remote_control_websocket_request( "x-codex-protocol-version", REMOTE_CONTROL_PROTOCOL_VERSION, )?; - let mut auth_headers = tungstenite::http::HeaderMap::new(); - auth.auth_provider.add_auth_headers(&mut auth_headers); - headers.extend(auth_headers); - set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?; + set_remote_control_header( + headers, + "authorization", + &format!( + "Bearer {}", + enrollment + .remote_control_token + .as_deref() + .ok_or_else(|| io::Error::other("missing remote control server token"))? + ), + )?; set_remote_control_header( headers, REMOTE_CONTROL_INSTALLATION_ID_HEADER, @@ -1063,7 +1225,7 @@ fn next_reconnect_delay(reconnect_attempt: &mut u64) -> (std::time::Duration, bo pub(super) async fn connect_remote_control_websocket( remote_control_target: &RemoteControlTarget, state_db: Option<&StateRuntime>, - auth_context: RemoteControlAuthContext<'_>, + mut auth_context: RemoteControlAuthContext<'_>, enrollment: &mut Option, connect_options: RemoteControlConnectOptions<'_>, status_publisher: &RemoteControlStatusPublisher, @@ -1072,11 +1234,6 @@ pub(super) async fn connect_remote_control_websocket( tungstenite::http::Response<()>, )> { ensure_rustls_crypto_provider(); - let RemoteControlAuthContext { - auth_manager, - auth_recovery, - auth_change_rx, - } = auth_context; let Some(state_db) = state_db else { *enrollment = None; @@ -1086,7 +1243,7 @@ pub(super) async fn connect_remote_control_websocket( )); }; - let auth = match load_remote_control_auth(auth_manager).await { + let auth = match load_remote_control_auth(auth_context.auth_manager).await { Ok(auth) => auth, Err(err) => { if err.kind() == ErrorKind::PermissionDenied { @@ -1131,52 +1288,87 @@ pub(super) async fn connect_remote_control_websocket( }); } - if enrollment.is_none() { + enroll_remote_control_server_if_missing( + remote_control_target, + state_db, + &auth, + &mut auth_context, + enrollment, + connect_options, + status_publisher, + ) + .await?; + + if enrollment + .as_ref() + .ok_or_else(|| io::Error::other("missing remote control enrollment after enrollment step"))? + .should_refresh_server_token() + { + let enrollment_ref = enrollment.as_ref().ok_or_else(|| { + io::Error::other("missing remote control enrollment after enrollment step") + })?; + let server_id = enrollment_ref.server_id.clone(); + let environment_id = enrollment_ref.environment_id.clone(); + info!( - "creating new remote control enrollment: websocket_url={}, enroll_url={}, account_id={}", - remote_control_target.websocket_url, remote_control_target.enroll_url, auth.account_id + "refreshing remote control server token: websocket_url={}, refresh_url={}, account_id={}, server_id={}, environment_id={}", + remote_control_target.websocket_url, + remote_control_target.refresh_url, + auth.account_id, + server_id, + environment_id ); - let new_enrollment = match enroll_remote_control_server( + let enrollment_ref = enrollment.as_mut().ok_or_else(|| { + io::Error::other("missing remote control enrollment before server refresh") + })?; + match refresh_remote_control_server( remote_control_target, &auth, connect_options.installation_id, - connect_options.server_name, + enrollment_ref, ) .await { - Ok(new_enrollment) => new_enrollment, + Ok(()) => {} + Err(err) if err.kind() == ErrorKind::NotFound => { + info!( + "remote control server refresh returned HTTP 404; clearing stale enrollment before re-enrolling: websocket_url={}, account_id={}, server_id={}, environment_id={}", + remote_control_target.websocket_url, auth.account_id, server_id, environment_id + ); + clear_remote_control_enrollment( + state_db, + remote_control_target, + &auth.account_id, + connect_options.app_server_client_name, + enrollment, + status_publisher, + ) + .await; + enroll_remote_control_server_if_missing( + remote_control_target, + state_db, + &auth, + &mut auth_context, + enrollment, + connect_options, + status_publisher, + ) + .await?; + } Err(err) if err.kind() == ErrorKind::PermissionDenied - && recover_remote_control_auth(auth_recovery, auth_change_rx).await => + && recover_remote_control_auth( + auth_context.auth_recovery, + auth_context.auth_change_rx, + ) + .await => { return Err(io::Error::other(format!( "{err}; retrying after auth recovery" ))); } Err(err) => return Err(err), - }; - if let Err(err) = update_persisted_remote_control_enrollment( - Some(state_db), - remote_control_target, - &auth.account_id, - connect_options.app_server_client_name, - Some(&new_enrollment), - ) - .await - { - return Err(io::Error::other(format!( - "failed to persist remote control enrollment in sqlite state db: {err}" - ))); } - info!( - "created new remote control enrollment: websocket_url={}, account_id={}, server_id={}, environment_id={}", - remote_control_target.websocket_url, - new_enrollment.account_id, - new_enrollment.server_id, - new_enrollment.environment_id - ); - status_publisher.publish_environment_id(Some(new_enrollment.environment_id.clone())); - *enrollment = Some(new_enrollment); } let enrollment_ref = enrollment.as_ref().ok_or_else(|| { @@ -1185,12 +1377,26 @@ pub(super) async fn connect_remote_control_websocket( let request = build_remote_control_websocket_request( &remote_control_target.websocket_url, enrollment_ref, - &auth, connect_options.installation_id, connect_options.subscribe_cursor, )?; - match connect_async(request).await { + let websocket_connect_result = tokio::time::timeout( + REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT, + connect_async(request), + ) + .await + .map_err(|_| { + io::Error::new( + ErrorKind::TimedOut, + format!( + "timed out connecting to remote control websocket at `{}` after {:?}", + remote_control_target.websocket_url, REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT + ), + ) + })?; + + match websocket_connect_result { Ok((websocket_stream, response)) => Ok((websocket_stream, response.map(|_| ()))), Err(err) => { match &err { @@ -1202,31 +1408,31 @@ pub(super) async fn connect_remote_control_websocket( enrollment_ref.server_id, enrollment_ref.environment_id ); - if let Err(clear_err) = update_persisted_remote_control_enrollment( - Some(state_db), + clear_remote_control_enrollment( + state_db, remote_control_target, &auth.account_id, connect_options.app_server_client_name, - /*enrollment*/ None, + enrollment, + status_publisher, ) - .await - { - warn!( - "failed to clear stale remote control enrollment in sqlite state db: {clear_err}" - ); - } - *enrollment = None; - status_publisher.publish_environment_id(/*environment_id*/ None); + .await; } tungstenite::Error::Http(response) if matches!(response.status().as_u16(), 401 | 403) => { - if recover_remote_control_auth(auth_recovery, auth_change_rx).await { - return Err(io::Error::other(format!( - "remote control websocket auth failed with HTTP {}; retrying after auth recovery", - response.status() - ))); - } + enrollment + .as_mut() + .ok_or_else(|| { + io::Error::other( + "missing remote control enrollment after websocket auth failure", + ) + })? + .clear_server_token(); + return Err(io::Error::other(format!( + "remote control websocket auth failed with HTTP {}; refreshing server token before reconnect", + response.status() + ))); } _ => {} } @@ -1240,6 +1446,94 @@ pub(super) async fn connect_remote_control_websocket( } } +async fn clear_remote_control_enrollment( + state_db: &StateRuntime, + remote_control_target: &RemoteControlTarget, + account_id: &str, + app_server_client_name: Option<&str>, + enrollment: &mut Option, + status_publisher: &RemoteControlStatusPublisher, +) { + if let Err(clear_err) = update_persisted_remote_control_enrollment( + Some(state_db), + remote_control_target, + account_id, + app_server_client_name, + /*enrollment*/ None, + ) + .await + { + warn!("failed to clear stale remote control enrollment in sqlite state db: {clear_err}"); + } + *enrollment = None; + status_publisher.publish_environment_id(/*environment_id*/ None); +} + +async fn enroll_remote_control_server_if_missing( + remote_control_target: &RemoteControlTarget, + state_db: &StateRuntime, + auth: &RemoteControlConnectionAuth, + auth_context: &mut RemoteControlAuthContext<'_>, + enrollment: &mut Option, + connect_options: RemoteControlConnectOptions<'_>, + status_publisher: &RemoteControlStatusPublisher, +) -> io::Result<()> { + if enrollment.is_some() { + return Ok(()); + } + + info!( + "creating new remote control enrollment: websocket_url={}, enroll_url={}, account_id={}", + remote_control_target.websocket_url, remote_control_target.enroll_url, auth.account_id + ); + let new_enrollment = match enroll_remote_control_server( + remote_control_target, + auth, + connect_options.installation_id, + connect_options.server_name, + ) + .await + { + Ok(new_enrollment) => new_enrollment, + Err(err) + if err.kind() == ErrorKind::PermissionDenied + && recover_remote_control_auth( + auth_context.auth_recovery, + auth_context.auth_change_rx, + ) + .await => + { + return Err(io::Error::other(format!( + "{err}; retrying after auth recovery" + ))); + } + Err(err) => return Err(err), + }; + if let Err(err) = update_persisted_remote_control_enrollment( + Some(state_db), + remote_control_target, + &auth.account_id, + connect_options.app_server_client_name, + Some(&new_enrollment), + ) + .await + { + return Err(io::Error::other(format!( + "failed to persist remote control enrollment in sqlite state db: {err}" + ))); + } + info!( + "created new remote control enrollment: websocket_url={}, account_id={}, server_id={}, environment_id={}", + remote_control_target.websocket_url, + new_enrollment.account_id, + new_enrollment.server_id, + new_enrollment.environment_id + ); + status_publisher.publish_environment_id(Some(new_enrollment.environment_id.clone())); + *enrollment = Some(new_enrollment); + Ok(()) +} + async fn recover_remote_control_auth( auth_recovery: &mut UnauthorizedRecovery, auth_change_rx: &mut watch::Receiver, @@ -1349,6 +1643,19 @@ mod tests { #[cfg(not(windows))] const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(5); const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; + const TEST_REMOTE_CONTROL_SERVER_TOKEN: &str = "Remote Control Token"; + + fn remote_control_enrollment(remote_control_token: Option<&str>) -> RemoteControlEnrollment { + RemoteControlEnrollment { + account_id: "account_id".to_string(), + environment_id: "env_test".to_string(), + server_id: "srv_e_test".to_string(), + server_name: "test-server".to_string(), + remote_control_token: remote_control_token.map(str::to_string), + expires_at: remote_control_token + .map(|_| time::OffsetDateTime::now_utc() + time::Duration::hours(1)), + } + } #[test] fn next_reconnect_delay_resets_after_cap() { @@ -1500,12 +1807,9 @@ mod tests { let auth_manager = remote_control_auth_manager(); let mut auth_recovery = auth_manager.unauthorized_recovery(); let mut auth_change_rx = auth_manager.auth_change_receiver(); - let mut enrollment = Some(RemoteControlEnrollment { - account_id: "account_id".to_string(), - environment_id: "env_test".to_string(), - server_id: "srv_e_test".to_string(), - server_name: "test-server".to_string(), - }); + let mut enrollment = Some(remote_control_enrollment(Some( + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ))); let (status_publisher, status_rx) = remote_control_status_channel(); let err = match connect_remote_control_websocket( @@ -1545,7 +1849,7 @@ mod tests { } #[tokio::test] - async fn connect_remote_control_websocket_recovers_after_unauthorized_reload() { + async fn connect_remote_control_websocket_invalidates_unauthorized_server_token() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); @@ -1553,35 +1857,14 @@ mod tests { let remote_control_target = normalize_remote_control_url(&remote_control_url).expect("target should parse"); let codex_home = TempDir::new().expect("temp dir should create"); - save_auth( - codex_home.path(), - &remote_control_auth_dot_json("stale-token"), - AuthCredentialsStoreMode::File, - ) - .expect("stale auth should save"); let state_db = remote_control_state_runtime(&codex_home).await; - let auth_manager = AuthManager::shared( - codex_home.path().to_path_buf(), - /*enable_codex_api_key_env*/ false, - AuthCredentialsStoreMode::File, - /*chatgpt_base_url*/ None, - ) - .await; + let auth_manager = remote_control_auth_manager(); let mut auth_recovery = auth_manager.unauthorized_recovery(); let mut auth_change_rx = auth_manager.auth_change_receiver(); - let mut enrollment = Some(RemoteControlEnrollment { - account_id: "account_id".to_string(), - environment_id: "env_test".to_string(), - server_id: "srv_e_test".to_string(), - server_name: "test-server".to_string(), - }); + let mut enrollment = Some(remote_control_enrollment(Some( + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ))); let (status_publisher, status_rx) = remote_control_status_channel(); - save_auth( - codex_home.path(), - &remote_control_auth_dot_json("fresh-token"), - AuthCredentialsStoreMode::File, - ) - .expect("fresh auth should save"); let server_task = tokio::spawn(async move { let (stream, request_line) = accept_http_request(&listener).await; @@ -1624,22 +1907,13 @@ mod tests { ); assert_eq!( err.to_string(), - "remote control websocket auth failed with HTTP 401 Unauthorized; retrying after auth recovery" + "remote control websocket auth failed with HTTP 401 Unauthorized; refreshing server token before reconnect" ); assert_eq!( - auth_manager - .auth() - .await - .expect("auth should remain available") - .get_token() - .expect("token should be readable"), - "fresh-token" - ); - assert!( - !auth_change_rx - .has_changed() - .expect("auth change watch should remain open"), - "recovery's own auth reload should not wake the reconnect loop" + enrollment, + Some(remote_control_enrollment( + /*remote_control_token*/ None + )) ); } @@ -1735,6 +2009,104 @@ mod tests { ); } + #[tokio::test] + async fn connect_remote_control_websocket_recovers_after_unauthorized_refresh() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let remote_control_url = remote_control_url_for_listener(&listener); + let remote_control_target = + normalize_remote_control_url(&remote_control_url).expect("target should parse"); + let refresh_url = remote_control_target.refresh_url.clone(); + let server_task = tokio::spawn(async move { + let (stream, request_line) = accept_http_request(&listener).await; + assert_eq!( + request_line, + "POST /backend-api/wham/remote/control/server/refresh HTTP/1.1" + ); + respond_with_status_and_headers(stream, "401 Unauthorized", &[], "unauthorized").await; + }); + let codex_home = TempDir::new().expect("temp dir should create"); + save_auth( + codex_home.path(), + &remote_control_auth_dot_json("stale-token"), + AuthCredentialsStoreMode::File, + ) + .expect("stale auth should save"); + let state_db = remote_control_state_runtime(&codex_home).await; + let auth_manager = AuthManager::shared( + codex_home.path().to_path_buf(), + /*enable_codex_api_key_env*/ false, + AuthCredentialsStoreMode::File, + /*chatgpt_base_url*/ None, + ) + .await; + let mut auth_recovery = auth_manager.unauthorized_recovery(); + let mut auth_change_rx = auth_manager.auth_change_receiver(); + let mut enrollment = Some(remote_control_enrollment( + /*remote_control_token*/ None, + )); + let (status_publisher, status_rx) = remote_control_status_channel(); + save_auth( + codex_home.path(), + &remote_control_auth_dot_json("fresh-token"), + AuthCredentialsStoreMode::File, + ) + .expect("fresh auth should save"); + + let err = connect_remote_control_websocket( + &remote_control_target, + Some(state_db.as_ref()), + RemoteControlAuthContext { + auth_manager: &auth_manager, + auth_recovery: &mut auth_recovery, + auth_change_rx: &mut auth_change_rx, + }, + &mut enrollment, + RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, + server_name: "test-server", + subscribe_cursor: None, + app_server_client_name: None, + }, + &status_publisher, + ) + .await + .expect_err("unauthorized refresh should fail the websocket connect"); + + server_task.await.expect("server task should succeed"); + assert_eq!( + status_rx.borrow().clone(), + RemoteControlStatusChangedNotification { + status: RemoteControlConnectionStatus::Connecting, + server_name: "test-server".to_string(), + installation_id: TEST_INSTALLATION_ID.to_string(), + environment_id: Some("env_test".to_string()), + } + ); + assert_eq!( + err.to_string(), + format!( + "remote control server refresh failed at `{refresh_url}`: HTTP 401 Unauthorized, request-id: , cf-ray: , body: unauthorized; retrying after auth recovery" + ) + ); + assert_eq!( + auth_manager + .auth() + .await + .expect("auth should remain available") + .get_token() + .expect("token should be readable"), + "fresh-token" + ); + assert!( + !auth_change_rx + .has_changed() + .expect("auth change watch should remain open"), + "recovery's own auth reload should not wake the reconnect loop" + ); + } + #[tokio::test] async fn connect_remote_control_websocket_requires_sqlite_state_db() { let remote_control_target = normalize_remote_control_url("http://127.0.0.1:9/backend-api/") @@ -1742,12 +2114,9 @@ mod tests { let auth_manager = remote_control_auth_manager(); let mut auth_recovery = auth_manager.unauthorized_recovery(); let mut auth_change_rx = auth_manager.auth_change_receiver(); - let mut enrollment = Some(RemoteControlEnrollment { - account_id: "account_id".to_string(), - environment_id: "env_test".to_string(), - server_id: "srv_e_test".to_string(), - server_name: "test-server".to_string(), - }); + let mut enrollment = Some(remote_control_enrollment(Some( + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ))); let (status_publisher, _status_rx) = remote_control_status_channel(); let err = connect_remote_control_websocket( @@ -1790,12 +2159,9 @@ mod tests { .await; let mut auth_recovery = auth_manager.unauthorized_recovery(); let mut auth_change_rx = auth_manager.auth_change_receiver(); - let mut enrollment = Some(RemoteControlEnrollment { - account_id: "account_id".to_string(), - environment_id: "env_test".to_string(), - server_id: "srv_e_test".to_string(), - server_name: "test-server".to_string(), - }); + let mut enrollment = Some(remote_control_enrollment(Some( + TEST_REMOTE_CONTROL_SERVER_TOKEN, + ))); let (status_publisher, mut status_rx) = remote_control_status_channel(); status_publisher.publish_environment_id(Some("env_test".to_string())); status_rx @@ -2013,6 +2379,17 @@ mod tests { .expect("writer should stop cleanly"); } + #[tokio::test] + async fn join_connection_workers_aborts_stuck_worker_after_timeout() { + let mut join_set = tokio::task::JoinSet::new(); + join_set.spawn(futures::future::pending::<()>()); + + RemoteControlWebsocket::join_connection_workers(&mut join_set, Duration::from_millis(10)) + .await; + + assert!(join_set.is_empty()); + } + #[tokio::test] async fn run_server_writer_inner_assigns_contiguous_seq_ids_per_stream() { let (client_stream, mut server_stream) = connected_websocket_pair().await; @@ -2350,6 +2727,65 @@ mod tests { &raw[split..], ); + assert!(matches!( + observe_client_message(&mut state, first_chunk.clone()), + ClientSegmentObservation::Pending + )); + let completed_envelope = match observe_client_message(&mut state, second_chunk) { + ClientSegmentObservation::Forward(client_envelope) => *client_envelope, + _ => panic!("expected completed client message"), + }; + state.record_client_message_delivery( + &completed_envelope, + Some(( + ( + ClientId("client-1".to_string()), + Some(StreamId("stream-1".to_string())), + ), + 4, + )), + ); + assert!(matches!( + observe_client_message(&mut state, first_chunk), + ClientSegmentObservation::Dropped + )); + } + + #[test] + fn websocket_state_allows_replay_before_completed_chunk_delivery() { + let (outbound_buffer, _used_rx) = BoundedOutboundBuffer::new(); + let mut state = WebsocketState { + outbound_buffer, + subscribe_cursor: None, + next_seq_id_by_stream: HashMap::new(), + last_completed_client_chunk_seq_id_by_stream: HashMap::new(), + client_segment_reassembler: ClientSegmentReassembler::default(), + }; + let message = JSONRPCMessage::Notification(JSONRPCNotification { + method: "initialized".to_string(), + params: None, + }); + let raw = serde_json::to_vec(&message).expect("message should serialize"); + let split = raw.len() / 2; + let first_chunk = client_chunk_envelope( + "client-1", + "stream-1", + /*seq_id*/ 4, + /*segment_id*/ 0, + /*segment_count*/ 2, + raw.len(), + &raw[..split], + ); + let second_chunk = client_chunk_envelope( + "client-1", + "stream-1", + /*seq_id*/ 4, + /*segment_id*/ 1, + /*segment_count*/ 2, + raw.len(), + &raw[split..], + ); + assert!(matches!( observe_client_message(&mut state, first_chunk.clone()), ClientSegmentObservation::Pending @@ -2360,7 +2796,7 @@ mod tests { )); assert!(matches!( observe_client_message(&mut state, first_chunk), - ClientSegmentObservation::Dropped + ClientSegmentObservation::Pending )); } diff --git a/codex-rs/app-server-transport/src/transport/websocket.rs b/codex-rs/app-server-transport/src/transport/websocket.rs index 177e378906..5677faae67 100644 --- a/codex-rs/app-server-transport/src/transport/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/websocket.rs @@ -347,7 +347,7 @@ async fn run_websocket_inbound_loop( incoming_message = websocket_reader.next() => { match incoming_message { Some(Ok(message)) => match message.into_incoming() { - Some(IncomingWebSocketMessage::Text(text)) => { + Some(IncomingWebSocketMessage::Text(text)) if !forward_incoming_message( &transport_event_tx, &writer_tx_for_reader, @@ -355,10 +355,10 @@ async fn run_websocket_inbound_loop( &text, ) .await - { - break; - } + => { + break; } + Some(IncomingWebSocketMessage::Text(_)) => {} Some(IncomingWebSocketMessage::Ping(payload)) => { match writer_control_tx.try_send(M::pong(payload)) { Ok(()) => {} diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 95baac4e9e..37cdf219bf 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -32,7 +32,7 @@ axum = { workspace = true, default-features = false, features = [ ] } codex-analytics = { workspace = true } codex-arg0 = { workspace = true } -codex-cloud-requirements = { workspace = true } +codex-cloud-config = { workspace = true } codex-config = { workspace = true } codex-core = { workspace = true } codex-core-plugins = { workspace = true } @@ -54,7 +54,9 @@ codex-backend-client = { workspace = true } codex-file-search = { workspace = true } codex-chatgpt = { workspace = true } codex-login = { workspace = true } +codex-image-generation-extension = { workspace = true } codex-memories-extension = { workspace = true } +codex-web-search-extension = { workspace = true } codex-memories-write = { workspace = true } codex-mcp = { workspace = true } codex-model-provider = { workspace = true } @@ -93,6 +95,9 @@ tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } uuid = { workspace = true, features = ["serde", "v7"] } +[target.'cfg(windows)'.dependencies] +codex-windows-sandbox = { workspace = true } + [dev-dependencies] app_test_support = { workspace = true } axum = { workspace = true, default-features = false, features = [ diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index db76e363ec..29acc51983 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -23,7 +23,7 @@ Similar to [MCP](https://modelcontextprotocol.io/), `codex app-server` supports Supported transports: -- stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL) +- stdio (`--stdio` or `--listen stdio://`, default): newline-delimited JSON (JSONL) - websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**) - unix socket (`--listen unix://` or `--listen unix://PATH`): websocket connections over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path, using the standard HTTP Upgrade handshake - off (`--listen off`): do not expose a local transport @@ -134,7 +134,7 @@ Example with notification opt-out: - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. - `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. -- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. +- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate control/spawn parent is known. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`. @@ -158,9 +158,9 @@ Example with notification opt-out: - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective turn cwd. Prefer experimental `permissions` profile selection by id for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success. -- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`. +- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. `clientUserMessageId` is optional; when supplied, the corresponding `userMessage` item echoes it as `clientId`. Review and manual compaction turns reject `turn/steer`. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. - `thread/realtime/start` — start a thread-scoped realtime session (experimental); pass `outputModality: "text"` or `outputModality: "audio"` to choose model output, returns `{}` and streams `thread/realtime/*` notifications. Omit `transport` for the websocket transport, or pass `{ "type": "webrtc", "sdp": "..." }` to create a WebRTC session from a browser-generated SDP offer; the remote answer SDP is emitted as `thread/realtime/sdp`. - `thread/realtime/appendAudio` — append an input audio chunk to the active realtime session (experimental); returns `{}`. @@ -192,10 +192,11 @@ Example with notification opt-out: - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. -- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. +- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). +- `skills/extraRoots/set` — replace the app-server process runtime extra standalone skill roots. The roots are not persisted; missing directories are accepted and simply load no skills. - `hooks/list` — list discovered hooks for one or more `cwd` values. - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. - `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists. @@ -216,7 +217,7 @@ Example with notification opt-out: - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. -- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. +- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. - `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. - `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. @@ -281,6 +282,8 @@ To continue a stored session, call `thread/resume` with the `thread.id` you prev By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Experimental clients can pass `excludeTurns: true` to return only thread metadata and live resume state, then call `thread/turns/list` separately if they want to page the turn history over the network. In that mode the server also skips replaying restored `thread/tokenUsage/updated`, which avoids rebuilding turns just to attribute historical usage. +Experimental clients that want the live resume subscription plus a turns page in one round trip can pass `initialTurnsPage`. It accepts the same `limit`, `sortDirection`, and `itemsView` controls as `thread/turns/list`; omitted controls use its defaults. The response includes `initialTurnsPage` with `nextCursor` and `backwardsCursor` for follow-up pagination. + By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead. Example: @@ -297,6 +300,24 @@ Example: "excludeTurns": true } } { "id": 12, "result": { "thread": { "id": "thr_123", "turns": [], … } } } + +{ "method": "thread/resume", "id": 13, "params": { + "threadId": "thr_123", + "excludeTurns": true, + "initialTurnsPage": { + "limit": 20, + "sortDirection": "desc", + "itemsView": "summary" + } +} } +{ "id": 13, "result": { + "thread": { "id": "thr_123", "turns": [], … }, + "initialTurnsPage": { + "data": [ ... ], + "nextCursor": "older-turns-cursor-or-null", + "backwardsCursor": "newer-turns-cursor-or-null" + } +} } ``` To branch from a stored session, call `thread/fork` with the `thread.id`. This creates a new thread id and emits a `thread/started` notification for it. The returned `thread.sessionId` identifies the current live session tree root. Root threads use their own `thread.id` as `thread.sessionId`; stored threads that are not loaded also report their own `thread.id`, because resuming one makes it the root of a new live session tree. When the source history includes persisted token usage, the server also emits `thread/tokenUsage/updated` for the new thread immediately after the response. If the source thread is actively running, the fork snapshots it as if the current turn had been interrupted first. Pass `ephemeral: true` when the fork should stay in-memory only: @@ -403,7 +424,7 @@ Later, after the idle unload timeout: ### Example: Read a thread -Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. +Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want thread history loaded into `thread.turns`. The returned thread includes `parentThreadId`, `agentNickname`, and `agentRole` for subagent threads when available. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } @@ -642,6 +663,7 @@ You can optionally specify config overrides on the new turn. If specified, these ```json { "method": "turn/start", "id": 30, "params": { "threadId": "thr_123", + "clientUserMessageId": "client_msg_123", "input": [ { "type": "text", "text": "Run tests" } ], // Below are optional config overrides "cwd": "/Users/me/project", @@ -840,6 +862,7 @@ not emit `turn/started` and does not accept thread settings overrides. ```json { "method": "turn/steer", "id": 32, "params": { "threadId": "thr_123", + "clientUserMessageId": "client_msg_124", "input": [ { "type": "text", "text": "Actually focus on failing tests first." } ], "expectedTurnId": "turn_456" } } @@ -1225,7 +1248,7 @@ Today both notifications carry an empty `items` array even when item events were `ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items: -- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`). +- `userMessage` — `{id, clientId, content}` where `clientId` is the optional `clientUserMessageId` supplied to `turn/start` or `turn/steer`, and `content` is a list of user inputs (`text`, `image`, or `localImage`). - `agentMessage` — `{id, text}` containing the accumulated agent reply. - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). @@ -1484,6 +1507,7 @@ $skill-creator Add a new skill for triaging flaky CI and include step-by-step us Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, with `forceReload`). `skills/list` might reuse a cached skills result per `cwd`; setting `forceReload` to `true` refreshes the result from disk. The server also emits `skills/changed` notifications when watched local skill files change. Treat this as an invalidation signal and re-run `skills/list` with your current params when needed. +Use `skills/extraRoots/set` to replace additional standalone skill roots for the current app-server process. These roots use the same layout as other standalone skill roots: each root contains skill directories, and each skill directory contains `SKILL.md`. Missing roots are accepted and load no skills until they exist. This setting is lost when app-server exits. ```json { "method": "skills/list", "id": 25, "params": { @@ -1520,12 +1544,23 @@ The server also emits `skills/changed` notifications when watched local skill fi } ``` +```json +{ + "method": "skills/extraRoots/set", + "id": 26, + "params": { + "extraRoots": ["/Users/me/generated-skills"] + } +} +{ "id": 26, "result": {} } +``` + To enable or disable a skill by absolute path: ```json { "method": "skills/config/write", - "id": 26, + "id": 27, "params": { "path": "/Users/alice/.codex/skills/skill-creator/SKILL.md", "name": null, @@ -1539,7 +1574,7 @@ To enable or disable a skill by name: ```json { "method": "skills/config/write", - "id": 27, + "id": 28, "params": { "path": null, "name": "github:yeet", diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 5f784ea23a..56754868fc 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -2095,6 +2095,7 @@ mod tests { use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; + use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -2110,7 +2111,6 @@ mod tests { use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow; use codex_protocol::protocol::RolloutItem; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; @@ -2153,6 +2153,7 @@ mod tests { let created_at = Utc::now(); let history_items = vec![ RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "before rollback".to_string(), images: None, local_images: Vec::new(), @@ -2169,6 +2170,7 @@ mod tests { thread_id, rollout_path: None, forked_from_id: None, + parent_thread_id: None, preview: "fallback preview".to_string(), name: Some("Rollback thread".to_string()), model_provider: "openai".to_string(), @@ -2186,7 +2188,7 @@ mod tests { agent_path: None, git_info: None, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: Some("before rollback".to_string()), history: Some(StoredThreadHistory { @@ -3228,6 +3230,7 @@ mod tests { state.track_current_turn_event( "turn-1", &EventMsg::UserMessage(codex_protocol::protocol::UserMessageEvent { + client_id: None, message: "already tracked".to_string(), images: None, local_images: Vec::new(), diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 443117e592..2f62f099e9 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -697,12 +697,13 @@ mod tests { let cwd = AbsolutePathBuf::current_dir().expect("current dir"); ExecRequest::new( vec!["cmd".to_string()], - cwd, + cwd.clone(), HashMap::new(), /*network*/ None, ExecExpiration::DefaultTimeout, codex_core::exec::ExecCapturePolicy::ShellTool, SandboxType::WindowsRestrictedToken, + vec![cwd], WindowsSandboxLevel::Disabled, /*windows_sandbox_private_desktop*/ false, PermissionProfile::read_only(), @@ -819,6 +820,7 @@ mod tests { ExecExpiration::Cancellation(CancellationToken::new()), codex_core::exec::ExecCapturePolicy::ShellTool, SandboxType::None, + vec![cwd.clone()], WindowsSandboxLevel::Disabled, /*windows_sandbox_private_desktop*/ false, PermissionProfile::read_only(), @@ -887,6 +889,7 @@ mod tests { }; let cancellation = CancellationToken::new(); let cancel = cancellation.clone(); + let cwd = AbsolutePathBuf::current_dir().expect("current dir"); manager .start(StartCommandExecParams { @@ -898,7 +901,7 @@ mod tests { process_id: Some("proc-101".to_string()), exec_request: ExecRequest::new( vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()], - AbsolutePathBuf::current_dir().expect("current dir"), + cwd.clone(), HashMap::new(), /*network*/ None, ExecExpiration::TimeoutOrCancellation { @@ -907,6 +910,7 @@ mod tests { }, codex_core::exec::ExecCapturePolicy::ShellTool, SandboxType::None, + vec![cwd], WindowsSandboxLevel::Disabled, /*windows_sandbox_private_desktop*/ false, PermissionProfile::read_only(), diff --git a/codex-rs/app-server/src/config_manager.rs b/codex-rs/app-server/src/config_manager.rs index 2cfd53b130..11633342b3 100644 --- a/codex-rs/app-server/src/config_manager.rs +++ b/codex-rs/app-server/src/config_manager.rs @@ -1,5 +1,5 @@ use codex_arg0::Arg0DispatchPaths; -use codex_cloud_requirements::cloud_requirements_loader; +use codex_cloud_config::cloud_requirements_loader; use codex_config::CloudRequirementsLoader; use codex_config::ConfigLayerStack; use codex_config::LoaderOverrides; diff --git a/codex-rs/app-server/src/config_manager_service.rs b/codex-rs/app-server/src/config_manager_service.rs index d2465d32dd..4b42c28aa9 100644 --- a/codex-rs/app-server/src/config_manager_service.rs +++ b/codex-rs/app-server/src/config_manager_service.rs @@ -624,6 +624,9 @@ fn override_message(layer: &ConfigLayerSource) -> String { ConfigLayerSource::System { file } => { format!("Overridden by managed config (system): {}", file.display()) } + ConfigLayerSource::EnterpriseManaged { id: _, name } => { + format!("Overridden by enterprise-managed config: {name}") + } ConfigLayerSource::Project { dot_codex_folder } => format!( "Overridden by project config: {}/{CONFIG_TOML_FILE}", dot_codex_folder.display(), diff --git a/codex-rs/app-server/src/extensions.rs b/codex-rs/app-server/src/extensions.rs index 9232cb758b..7b2673ca06 100644 --- a/codex-rs/app-server/src/extensions.rs +++ b/codex-rs/app-server/src/extensions.rs @@ -12,6 +12,7 @@ use codex_extension_api::AgentSpawner; use codex_extension_api::ExtensionEventSink; use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistryBuilder; +use codex_login::AuthManager; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::protocol::Event; @@ -22,6 +23,7 @@ use crate::outgoing_message::OutgoingMessageSender; pub(crate) fn thread_extensions( guardian_agent_spawner: S, event_sink: Arc, + auth_manager: Arc, ) -> Arc> where S: AgentSpawner + 'static, @@ -29,6 +31,8 @@ where let mut builder = ExtensionRegistryBuilder::::with_event_sink(event_sink); codex_guardian::install(&mut builder, guardian_agent_spawner); codex_memories_extension::install(&mut builder, codex_otel::global()); + codex_web_search_extension::install(&mut builder, auth_manager.clone()); + codex_image_generation_extension::install(&mut builder, auth_manager); Arc::new(builder.build()) } diff --git a/codex-rs/app-server/src/fs_watch.rs b/codex-rs/app-server/src/fs_watch.rs index 09c6dc5533..405b0560a3 100644 --- a/codex-rs/app-server/src/fs_watch.rs +++ b/codex-rs/app-server/src/fs_watch.rs @@ -8,66 +8,24 @@ use codex_app_server_protocol::FsWatchParams; use codex_app_server_protocol::FsWatchResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ServerNotification; +use codex_file_watcher::DebouncedWatchReceiver; use codex_file_watcher::FileWatcher; -use codex_file_watcher::FileWatcherEvent; use codex_file_watcher::FileWatcherSubscriber; -use codex_file_watcher::Receiver; use codex_file_watcher::WatchPath; use codex_file_watcher::WatchRegistration; use std::collections::HashMap; -use std::collections::HashSet; use std::collections::hash_map::Entry; use std::hash::Hash; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex as AsyncMutex; #[cfg(test)] use tokio::sync::mpsc; use tokio::sync::oneshot; -use tokio::time::Instant; use tracing::warn; const FS_CHANGED_NOTIFICATION_DEBOUNCE: Duration = Duration::from_millis(200); -struct DebouncedReceiver { - rx: Receiver, - interval: Duration, - changed_paths: HashSet, - next_allowance: Option, -} - -impl DebouncedReceiver { - fn new(rx: Receiver, interval: Duration) -> Self { - Self { - rx, - interval, - changed_paths: HashSet::new(), - next_allowance: None, - } - } - - async fn recv(&mut self) -> Option { - while self.changed_paths.is_empty() { - self.changed_paths.extend(self.rx.recv().await?.paths); - } - let next_allowance = *self - .next_allowance - .get_or_insert_with(|| Instant::now() + self.interval); - - loop { - tokio::select! { - event = self.rx.recv() => self.changed_paths.extend(event?.paths), - _ = tokio::time::sleep_until(next_allowance) => break, - } - } - - Some(FileWatcherEvent { - paths: self.changed_paths.drain().collect(), - }) - } -} - #[derive(Clone)] pub(crate) struct FsWatchManager { outgoing: Arc, @@ -151,7 +109,7 @@ impl FsWatchManager { let task_watch_id = watch_id.clone(); tokio::spawn(async move { - let mut rx = DebouncedReceiver::new(rx, FS_CHANGED_NOTIFICATION_DEBOUNCE); + let mut rx = DebouncedWatchReceiver::new(rx, FS_CHANGED_NOTIFICATION_DEBOUNCE); tokio::pin!(terminate_rx); loop { let event = tokio::select! { @@ -219,6 +177,8 @@ mod tests { use super::*; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; + use std::collections::HashSet; + use std::path::PathBuf; use tempfile::TempDir; fn absolute_path(path: PathBuf) -> AbsolutePathBuf { diff --git a/codex-rs/app-server/src/mcp_refresh.rs b/codex-rs/app-server/src/mcp_refresh.rs index d65b1d83e0..4ddfa34735 100644 --- a/codex-rs/app-server/src/mcp_refresh.rs +++ b/codex-rs/app-server/src/mcp_refresh.rs @@ -184,15 +184,16 @@ mod tests { let thread_manager = Arc::new_cyclic(|thread_manager| { ThreadManager::new( &good_config, - auth_manager, + auth_manager.clone(), SessionSource::Exec, Arc::new(EnvironmentManager::default_for_tests()), thread_extensions( guardian_agent_spawner(thread_manager.clone()), Arc::new(NoopExtensionEventSink), + auth_manager.clone(), ), /*analytics_events_client*/ None, - thread_store, + Arc::clone(&thread_store), Some(state_db.clone()), "11111111-1111-4111-8111-111111111111".to_string(), /*attestation_provider*/ None, diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 2d0e8d085f..2dbd17a3d7 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -314,6 +314,7 @@ impl MessageProcessor { thread_extensions( guardian_agent_spawner(thread_manager.clone()), app_server_extension_event_sink(outgoing.clone()), + auth_manager.clone(), ), Some(analytics_events_client.clone()), Arc::clone(&thread_store), @@ -353,6 +354,8 @@ impl MessageProcessor { app_list_shutdown_token, ); let catalog_processor = CatalogRequestProcessor::new( + outgoing.clone(), + Arc::clone(&skills_watcher), auth_manager.clone(), Arc::clone(&thread_manager), Arc::clone(&config), @@ -1107,6 +1110,9 @@ impl MessageProcessor { ClientRequest::SkillsList { params, .. } => { self.catalog_processor.skills_list(params).await } + ClientRequest::SkillsExtraRootsSet { params, .. } => { + self.catalog_processor.skills_extra_roots_set(params).await + } ClientRequest::HooksList { params, .. } => { self.catalog_processor.hooks_list(params).await } diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index a9625d3086..94b5a1af27 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -653,11 +653,13 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { params: TurnStartParams { environments: None, thread_id, + client_user_message_id: None, input: vec![UserInput::Text { text: "hello".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, cwd: None, runtime_workspace_roots: None, approval_policy: None, diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 6cab439bcc..e8fffd5361 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -456,8 +456,8 @@ impl OutgoingMessageSender { ) -> Vec { let request_id_to_callback = self.request_id_to_callback.lock().await; let mut requests = request_id_to_callback - .iter() - .filter_map(|(_, entry)| { + .values() + .filter_map(|entry| { (entry.thread_id == Some(thread_id)).then_some(entry.request.clone()) }) .collect::>(); diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index d0958e80ab..5d8cb4d794 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -25,6 +25,8 @@ use codex_app_server_protocol::AccountLoginCompletedNotification; use codex_app_server_protocol::AccountUpdatedNotification; use codex_app_server_protocol::AddCreditsNudgeCreditType; use codex_app_server_protocol::AddCreditsNudgeEmailStatus; +use codex_app_server_protocol::AdditionalContextEntry; +use codex_app_server_protocol::AdditionalContextKind; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppListUpdatedNotification; use codex_app_server_protocol::AppSummary; @@ -48,7 +50,6 @@ use codex_app_server_protocol::CommandExecWriteParams; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::ConversationGitInfo; use codex_app_server_protocol::ConversationSummary; -use codex_app_server_protocol::DeprecationNoticeNotification; use codex_app_server_protocol::DynamicToolSpec as ApiDynamicToolSpec; use codex_app_server_protocol::EnvironmentAddParams; use codex_app_server_protocol::EnvironmentAddResponse; @@ -154,6 +155,8 @@ use codex_app_server_protocol::ServerRequestResolvedNotification; use codex_app_server_protocol::SkillSummary; use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; +use codex_app_server_protocol::SkillsExtraRootsSetParams; +use codex_app_server_protocol::SkillsExtraRootsSetResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::SortDirection; @@ -211,6 +214,7 @@ use codex_app_server_protocol::ThreadRealtimeStartResponse; use codex_app_server_protocol::ThreadRealtimeStartTransport; use codex_app_server_protocol::ThreadRealtimeStopParams; use codex_app_server_protocol::ThreadRealtimeStopResponse; +use codex_app_server_protocol::ThreadResumeInitialTurnsPageParams; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadRollbackParams; @@ -297,7 +301,6 @@ use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode; use codex_core::windows_sandbox::WindowsSandboxSetupRequest; use codex_core::windows_sandbox::sandbox_setup_is_complete; -use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; use codex_core_plugins::PluginInstallError as CorePluginInstallError; use codex_core_plugins::PluginInstallRequest; use codex_core_plugins::PluginLoadOutcome; @@ -400,7 +403,6 @@ use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::UserInput as CoreInputItem; use codex_rmcp_client::perform_oauth_login_return_url; -use codex_rollout::EventPersistenceMode; use codex_rollout::is_persisted_rollout_item; use codex_rollout::state_db::StateDbHandle; use codex_rollout::state_db::reconcile_rollout; @@ -421,6 +423,7 @@ use codex_thread_store::ThreadStore; use codex_thread_store::ThreadStoreError; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP; +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::io::Error as IoError; @@ -531,7 +534,7 @@ pub(crate) use self::thread_summary::thread_settings_from_core_snapshot; pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec { let mut builder = ThreadHistoryBuilder::new(); for item in items { - if is_persisted_rollout_item(item, EventPersistenceMode::Limited) { + if is_persisted_rollout_item(item) { builder.handle_rollout_item(item); } } diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index ba6c535665..6f17e4614f 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -4,6 +4,8 @@ use futures::StreamExt; #[derive(Clone)] pub(crate) struct CatalogRequestProcessor { + pub(super) outgoing: Arc, + pub(super) skills_watcher: Arc, pub(super) auth_manager: Arc, pub(super) thread_manager: Arc, pub(super) config: Arc, @@ -96,6 +98,8 @@ fn errors_to_info( impl CatalogRequestProcessor { pub(crate) fn new( + outgoing: Arc, + skills_watcher: Arc, auth_manager: Arc, thread_manager: Arc, config: Arc, @@ -103,6 +107,8 @@ impl CatalogRequestProcessor { workspace_settings_cache: Arc, ) -> Self { Self { + outgoing, + skills_watcher, auth_manager, thread_manager, config, @@ -138,6 +144,15 @@ impl CatalogRequestProcessor { .map(|response| Some(response.into())) } + pub(crate) async fn skills_extra_roots_set( + &self, + params: SkillsExtraRootsSetParams, + ) -> Result, JSONRPCErrorError> { + self.skills_extra_roots_set_response(params) + .await + .map(|response| Some(response.into())) + } + pub(crate) async fn model_list( &self, params: ModelListParams, @@ -567,6 +582,24 @@ impl CatalogRequestProcessor { Ok(SkillsListResponse { data }) } + async fn skills_extra_roots_set_response( + &self, + params: SkillsExtraRootsSetParams, + ) -> Result { + let SkillsExtraRootsSetParams { extra_roots } = params; + self.skills_watcher + .register_runtime_extra_roots(&extra_roots); + self.thread_manager + .skills_manager() + .set_extra_roots(extra_roots); + self.outgoing + .send_server_notification(ServerNotification::SkillsChanged( + codex_app_server_protocol::SkillsChangedNotification {}, + )) + .await; + Ok(SkillsExtraRootsSetResponse {}) + } + /// Handle `hooks/list` by resolving hooks for each requested cwd. async fn hooks_list_response( &self, diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index 930cc6d18f..5b14f0b0c0 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -203,6 +203,7 @@ impl CommandExecRequestProcessor { network_proxy_spec, network_proxy_permission_profile, managed_network_requirements_enabled, + windows_sandbox_workspace_roots, ) = if let Some(permission_profile) = permission_profile { let overrides = ConfigOverrides { cwd: Some(cwd.to_path_buf()), @@ -230,6 +231,7 @@ impl CommandExecRequestProcessor { config.permissions.network.clone(), config.permissions.permission_profile().clone(), config.managed_network_requirements_enabled(), + config.effective_workspace_roots(), ) } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) { self.config @@ -255,6 +257,7 @@ impl CommandExecRequestProcessor { self.config.permissions.network.clone(), self.config.permissions.permission_profile().clone(), self.config.managed_network_requirements_enabled(), + self.config.effective_workspace_roots(), ) } else { ( @@ -262,6 +265,7 @@ impl CommandExecRequestProcessor { self.config.permissions.network.clone(), self.config.permissions.permission_profile().clone(), self.config.managed_network_requirements_enabled(), + self.config.effective_workspace_roots(), ) }; let started_network_proxy = match network_proxy_spec.as_ref() { @@ -318,6 +322,7 @@ impl CommandExecRequestProcessor { exec_params, &effective_permission_profile, &sandbox_cwd, + windows_sandbox_workspace_roots.as_slice(), &codex_linux_sandbox_exe, use_legacy_landlock, ) diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 72948270a6..3571026193 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -30,6 +30,7 @@ use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::NetworkUnixSocketPermission; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_chatgpt::connectors; use codex_config::ConfigRequirementsToml; use codex_config::HookEventsToml; @@ -54,6 +55,7 @@ const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ "mentions_v2", "plugins", "remote_control", + "remote_plugin", "tool_suggest", "tool_call_mcp_elicitation", ]; @@ -419,6 +421,23 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), + allowed_windows_sandbox_implementations: requirements.windows.and_then(|windows| { + windows + .allowed_sandbox_implementations + .map(|implementations| { + implementations + .into_iter() + .map(|implementation| match implementation { + codex_config::types::WindowsSandboxModeToml::Elevated => { + WindowsSandboxSetupMode::Elevated + } + codex_config::types::WindowsSandboxModeToml::Unelevated => { + WindowsSandboxSetupMode::Unelevated + } + }) + .collect() + }) + }), allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes @@ -610,7 +629,7 @@ fn map_network_unix_socket_permission_to_api( ) -> NetworkUnixSocketPermission { match permission { codex_config::NetworkUnixSocketPermissionToml::Allow => NetworkUnixSocketPermission::Allow, - codex_config::NetworkUnixSocketPermissionToml::None => NetworkUnixSocketPermission::None, + codex_config::NetworkUnixSocketPermissionToml::Deny => NetworkUnixSocketPermission::Deny, } } @@ -633,8 +652,10 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> #[cfg(test)] mod tests { use super::map_requirements_toml_to_api; + use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ComputerUseRequirementsToml; use codex_config::ConfigRequirementsToml; + use codex_config::WindowsRequirementsToml; use pretty_assertions::assert_eq; #[test] @@ -686,4 +707,25 @@ mod tests { Some(false) ); } + + #[test] + fn requirements_api_includes_allowed_windows_sandbox_implementations() { + let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { + windows: Some(WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + codex_config::types::WindowsSandboxModeToml::Elevated, + codex_config::types::WindowsSandboxModeToml::Unelevated, + ]), + }), + ..ConfigRequirementsToml::default() + }); + + assert_eq!( + mapped.allowed_windows_sandbox_implementations, + Some(vec![ + WindowsSandboxSetupMode::Elevated, + WindowsSandboxSetupMode::Unelevated, + ]) + ); + } } diff --git a/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs index 21b7da679b..87956182d0 100644 --- a/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs +++ b/codex-rs/app-server/src/request_processors/external_agent_config_processor.rs @@ -309,7 +309,6 @@ impl ExternalAgentConfigRequestProcessor { session_source: None, thread_source: None, dynamic_tools: Vec::new(), - persist_extended_history: false, metrics_service_name: None, parent_trace: None, environments, diff --git a/codex-rs/app-server/src/request_processors/feedback_processor.rs b/codex-rs/app-server/src/request_processors/feedback_processor.rs index 18f519d3c0..73da67f735 100644 --- a/codex-rs/app-server/src/request_processors/feedback_processor.rs +++ b/codex-rs/app-server/src/request_processors/feedback_processor.rs @@ -1,4 +1,6 @@ use super::*; +#[cfg(target_os = "windows")] +use codex_feedback::WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME; #[derive(Clone)] pub(crate) struct FeedbackRequestProcessor { @@ -186,6 +188,12 @@ impl FeedbackRequestProcessor { )), }); } + if let Some(sandbox_log_attachment) = + windows_sandbox_log_attachment(&self.config.codex_home) + && seen_attachment_paths.insert(sandbox_log_attachment.path.clone()) + { + attachment_paths.push(sandbox_log_attachment); + } } if let Some(extra_log_files) = extra_log_files { for extra_log_file in extra_log_files { @@ -264,3 +272,46 @@ impl FeedbackRequestProcessor { fn auto_review_rollout_filename(thread_id: ThreadId) -> String { format!("auto-review-rollout-{thread_id}.jsonl") } + +#[cfg(target_os = "windows")] +fn windows_sandbox_log_attachment(codex_home: &Path) -> Option { + let sandbox_log_path = codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home); + sandbox_log_path + .is_file() + .then_some(FeedbackAttachmentPath { + path: sandbox_log_path, + attachment_filename_override: Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string()), + }) +} + +#[cfg(not(target_os = "windows"))] +fn windows_sandbox_log_attachment(_codex_home: &Path) -> Option { + None +} + +#[cfg(all(test, target_os = "windows"))] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn windows_sandbox_log_attachment_uses_current_log() { + let codex_home = tempfile::tempdir().expect("create tempdir"); + let sandbox_dir = codex_windows_sandbox::sandbox_dir(codex_home.path()); + std::fs::create_dir_all(&sandbox_dir).expect("create sandbox dir"); + let sandbox_log_path = + codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home.path()); + std::fs::write(&sandbox_log_path, "sandbox log").expect("write sandbox log"); + + let attachment = windows_sandbox_log_attachment(codex_home.path()) + .map(|attachment| (attachment.path, attachment.attachment_filename_override)); + + assert_eq!( + attachment, + Some(( + sandbox_log_path, + Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string()) + )) + ); + } +} diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 22d5a4326a..ae62e2e785 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -276,6 +276,7 @@ impl McpRequestProcessor { .await; let McpServerStatusSnapshot { + server_infos, tools_by_server, resources, resource_templates, @@ -315,6 +316,7 @@ impl McpRequestProcessor { .iter() .map(|name| McpServerStatus { name: name.clone(), + server_info: server_infos.get(name).cloned(), tools: tools_by_server.get(name).cloned().unwrap_or_default(), resources: resources.get(name).cloned().unwrap_or_default(), resource_templates: resource_templates.get(name).cloned().unwrap_or_default(), diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index fd45a8a732..4171887cdd 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -6,6 +6,8 @@ use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginSharePrincipalRole; use codex_app_server_protocol::PluginShareTargetRole; use codex_config::types::McpServerConfig; +use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; +use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; use codex_core_plugins::remote::RemotePluginScope; use codex_core_plugins::remote::is_valid_remote_plugin_id; use codex_core_plugins::remote::validate_remote_plugin_id; @@ -156,6 +158,52 @@ fn remote_installed_plugin_visible_scopes(config: &Config) -> Vec, + prefer_remote_curated_conflicts: bool, +) { + let local_installed_plugin_names = marketplaces + .iter() + .find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) + .map(|marketplace| installed_plugin_names(&marketplace.plugins)) + .unwrap_or_default(); + let remote_installed_plugin_names = marketplaces + .iter() + .find(|marketplace| marketplace.name == REMOTE_GLOBAL_MARKETPLACE_NAME) + .map(|marketplace| installed_plugin_names(&marketplace.plugins)) + .unwrap_or_default(); + let conflicting_plugin_names = local_installed_plugin_names + .intersection(&remote_installed_plugin_names) + .cloned() + .collect::>(); + if conflicting_plugin_names.is_empty() { + return; + } + + let marketplace_to_filter = if prefer_remote_curated_conflicts { + OPENAI_CURATED_MARKETPLACE_NAME + } else { + REMOTE_GLOBAL_MARKETPLACE_NAME + }; + for marketplace in marketplaces.iter_mut() { + if marketplace.name != marketplace_to_filter { + continue; + } + marketplace + .plugins + .retain(|plugin| !plugin.installed || !conflicting_plugin_names.contains(&plugin.name)); + } + marketplaces.retain(|marketplace| !marketplace.plugins.is_empty()); +} + +fn installed_plugin_names(plugins: &[PluginSummary]) -> HashSet { + plugins + .iter() + .filter(|plugin| plugin.installed) + .map(|plugin| plugin.name.clone()) + .collect() +} + fn remote_plugin_share_discoverability( discoverability: PluginShareDiscoverability, ) -> codex_core_plugins::remote::RemotePluginShareDiscoverability { @@ -740,6 +788,10 @@ impl PluginRequestProcessor { ) .await, ); + filter_openai_curated_installed_conflicts( + &mut data, + config.features.enabled(Feature::RemotePlugin), + ); Ok(PluginInstalledResponse { marketplaces: data, @@ -1509,7 +1561,7 @@ impl PluginRequestProcessor { let environment_manager = self.thread_manager.environment_manager(); let (all_connectors_result, accessible_connectors_result) = tokio::join!( - connectors::list_all_connectors_with_options(config, /*force_refetch*/ true), + connectors::list_all_connectors_with_options(config, /*force_refetch*/ false), connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( config, /*force_refetch*/ true, diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index 985baac91a..f72cfb5a62 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -565,8 +565,28 @@ pub(super) async fn handle_pending_thread_resume_request( has_live_in_progress_turn, ); let token_usage_thread = pending.include_turns.then(|| thread.clone()); + let mut initial_turns_page = if let Some(params) = pending.initial_turns_page.as_ref() { + match super::thread_processor::build_thread_resume_initial_turns_page( + &pending.history_items, + thread.status.clone(), + has_live_in_progress_turn, + active_turn, + params, + ) { + Ok(page) => Some(page), + Err(error) => { + outgoing.send_error(request_id, error).await; + return; + } + } + } else { + None + }; if pending.redact_resume_payloads { - redact_thread_resume_payloads(&mut thread); + redact_thread_resume_payloads(&mut thread.turns); + if let Some(initial_turns_page) = initial_turns_page.as_mut() { + redact_thread_resume_payloads(&mut initial_turns_page.data); + } } { @@ -635,6 +655,7 @@ pub(super) async fn handle_pending_thread_resume_request( sandbox, active_permission_profile, reasoning_effort, + initial_turns_page, }; outgoing.send_response(request_id, response).await; // Match cold resume: metadata-only resume should attach the listener without diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index c9a42d8359..32dfb1eeb9 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -5,10 +5,6 @@ use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; const THREAD_LIST_DEFAULT_LIMIT: usize = 25; const THREAD_LIST_MAX_LIMIT: usize = 100; -const PERSIST_EXTENDED_HISTORY_DEPRECATION_SUMMARY: &str = - "persistExtendedHistory is deprecated and ignored"; -const PERSIST_EXTENDED_HISTORY_DEPRECATION_DETAILS: &str = - "Remove this parameter. App-server always uses limited history persistence."; struct ThreadListFilters { model_providers: Option>, @@ -849,17 +845,12 @@ impl ThreadRequestProcessor { session_start_source, thread_source, environments, - persist_extended_history, } = params; if sandbox.is_some() && permissions.is_some() { return Err(invalid_request( "`permissions` cannot be combined with `sandbox`", )); } - if persist_extended_history { - self.send_persist_extended_history_deprecation_notice(request_id.connection_id) - .await; - } let environment_selections = self.parse_environment_selections(environments)?; let mut typesafe_overrides = self.build_thread_config_overrides( model, @@ -952,18 +943,6 @@ impl ThreadRequestProcessor { self.outgoing.request_trace_context(request_id).await } - async fn send_persist_extended_history_deprecation_notice(&self, connection_id: ConnectionId) { - self.outgoing - .send_server_notification_to_connections( - &[connection_id], - ServerNotification::DeprecationNotice(DeprecationNoticeNotification { - summary: PERSIST_EXTENDED_HISTORY_DEPRECATION_SUMMARY.to_string(), - details: Some(PERSIST_EXTENDED_HISTORY_DEPRECATION_DETAILS.to_string()), - }), - ) - .await; - } - async fn submit_core_op( &self, request_id: &ConnectionRequestId, @@ -1106,7 +1085,6 @@ impl ThreadRequestProcessor { session_source: None, thread_source, dynamic_tools: core_dynamic_tools, - persist_extended_history: false, metrics_service_name: service_name, parent_trace: request_trace, environments, @@ -1115,7 +1093,6 @@ impl ThreadRequestProcessor { "app_server.thread_start.create_thread", otel.name = "app_server.thread_start.create_thread", thread_start.dynamic_tool_count = core_dynamic_tool_count, - thread_start.persist_extended_history = false, )) .await .map_err(|err| match err { @@ -1308,7 +1285,7 @@ impl ThreadRequestProcessor { params: ThreadArchiveParams, ) -> Result<(ThreadArchiveResponse, Vec), JSONRPCErrorError> { let thread_id = ThreadId::from_string(¶ms.thread_id) - .map_err(|err| invalid_request(format!("invalid thread id: {err}")))?; + .map_err(|err| invalid_request(format!("invalid session id: {err}")))?; let mut thread_ids = vec![thread_id]; if let Some(state_db_ctx) = self.state_db.as_ref() { @@ -1317,7 +1294,7 @@ impl ThreadRequestProcessor { .await .map_err(|err| { internal_error(format!( - "failed to list spawned descendants for thread id {thread_id}: {err}" + "failed to list spawned descendants for session {thread_id}: {err}" )) })?; let mut seen = HashSet::from([thread_id]); @@ -1629,7 +1606,7 @@ impl ThreadRequestProcessor { params: ThreadUnarchiveParams, ) -> Result<(ThreadUnarchiveResponse, String), JSONRPCErrorError> { let thread_id = ThreadId::from_string(¶ms.thread_id) - .map_err(|err| invalid_request(format!("invalid thread id: {err}")))?; + .map_err(|err| invalid_request(format!("invalid session id: {err}")))?; let fallback_provider = self.config.model_provider_id.clone(); let stored_thread = self @@ -2241,8 +2218,6 @@ impl ThreadRequestProcessor { sort_direction, items_view, } = params; - let items_view = items_view.unwrap_or(TurnItemsView::Summary); - let thread_uuid = ThreadId::from_string(&thread_id) .map_err(|err| invalid_request(format!("invalid thread id: {err}")))?; @@ -2270,60 +2245,20 @@ impl ThreadRequestProcessor { } else { None }; - let mut turns = reconstruct_thread_turns_for_turns_list( + build_thread_turns_page_response( &items, self.thread_watch_manager .loaded_status_for_thread(&thread_uuid.to_string()) .await, has_live_running_thread, active_turn, - ); - for turn in &mut turns { - match items_view { - TurnItemsView::NotLoaded => { - turn.items.clear(); - turn.items_view = TurnItemsView::NotLoaded; - } - TurnItemsView::Summary => { - let first_user_message = turn - .items - .iter() - .find(|item| matches!(item, ThreadItem::UserMessage { .. })) - .cloned(); - let final_agent_message = turn - .items - .iter() - .rev() - .find(|item| matches!(item, ThreadItem::AgentMessage { .. })) - .cloned(); - turn.items = match (first_user_message, final_agent_message) { - (Some(user_message), Some(agent_message)) - if user_message.id() != agent_message.id() => - { - vec![user_message, agent_message] - } - (Some(user_message), _) => vec![user_message], - (None, Some(agent_message)) => vec![agent_message], - (None, None) => Vec::new(), - }; - turn.items_view = TurnItemsView::Summary; - } - TurnItemsView::Full => { - turn.items_view = TurnItemsView::Full; - } - } - } - let page = paginate_thread_turns( - turns, - cursor.as_deref(), - limit, - sort_direction.unwrap_or(SortDirection::Desc), - )?; - Ok(ThreadTurnsListResponse { - data: page.turns, - next_cursor: page.next_cursor, - backwards_cursor: page.backwards_cursor, - }) + ThreadTurnsPageOptions { + cursor: cursor.as_deref(), + limit, + sort_direction: sort_direction.unwrap_or(SortDirection::Desc), + items_view: items_view.unwrap_or(TurnItemsView::Summary), + }, + ) } async fn load_thread_turns_list_history( @@ -2422,6 +2357,7 @@ impl ThreadRequestProcessor { thread_id: ThreadId, connection_ids: Vec, ) { + let mut raw_events_enabled = false; if let Ok(thread) = self.thread_manager.get_thread(thread_id).await { let config_snapshot = thread.config_snapshot().await; let loaded_thread = build_thread_from_snapshot( @@ -2431,16 +2367,21 @@ impl ThreadRequestProcessor { thread.rollout_path(), ); self.thread_watch_manager.upsert_thread(loaded_thread).await; + if let Some(parent_thread_id) = config_snapshot.parent_thread_id { + raw_events_enabled = self + .thread_state_manager + .thread_state(parent_thread_id) + .await + .lock() + .await + .experimental_raw_events; + } } for connection_id in connection_ids { log_listener_attach_result( - self.ensure_conversation_listener( - thread_id, - connection_id, - /*raw_events_enabled*/ false, - ) - .await, + self.ensure_conversation_listener(thread_id, connection_id, raw_events_enabled) + .await, thread_id, connection_id, "thread", @@ -2482,10 +2423,6 @@ impl ThreadRequestProcessor { .await; return Ok(()); } - if params.persist_extended_history { - self.send_persist_extended_history_deprecation_notice(request_id.connection_id) - .await; - } let redact_resume_payloads = should_redact_thread_resume_payloads(app_server_client_name.as_deref()); @@ -2531,7 +2468,7 @@ impl ThreadRequestProcessor { developer_instructions, personality, exclude_turns, - persist_extended_history: _persist_extended_history, + initial_turns_page, } = params; let include_turns = !exclude_turns; @@ -2596,7 +2533,6 @@ impl ThreadRequestProcessor { config.clone(), thread_history, self.auth_manager.clone(), - /*persist_extended_history*/ false, self.request_trace_context(&request_id).await, ) .await @@ -2685,8 +2621,28 @@ impl ThreadRequestProcessor { config_snapshot.active_permission_profile, ); let token_usage_thread = include_turns.then(|| thread.clone()); + let mut initial_turns_page = if let Some(params) = initial_turns_page.as_ref() { + match build_thread_resume_initial_turns_page( + &response_history.get_rollout_items(), + thread.status.clone(), + /*has_live_running_thread*/ false, + /*active_turn*/ None, + params, + ) { + Ok(page) => Some(page), + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return Ok(()); + } + } + } else { + None + }; if redact_resume_payloads { - redact_thread_resume_payloads(&mut thread); + redact_thread_resume_payloads(&mut thread.turns); + if let Some(initial_turns_page) = initial_turns_page.as_mut() { + redact_thread_resume_payloads(&mut initial_turns_page.data); + } } let response = ThreadResumeResponse { @@ -2702,6 +2658,7 @@ impl ThreadRequestProcessor { sandbox, active_permission_profile, reasoning_effort: session_configured.reasoning_effort, + initial_turns_page, }; let connection_id = request_id.connection_id; @@ -2787,15 +2744,20 @@ impl ThreadRequestProcessor { /*include_history*/ true, ) .await?; - if source_thread.thread_id != existing_thread_id { - return Err(invalid_request(format!( - "cannot resume running thread {existing_thread_id} from source thread {}", - source_thread.thread_id - ))); - } Some((existing_thread_id, existing_thread, source_thread)) } else { - None + let source_thread = self + .read_stored_thread_for_resume( + ¶ms.thread_id, + params.path.as_ref(), + /*include_history*/ true, + ) + .await?; + let existing_thread_id = source_thread.thread_id; + match self.thread_manager.get_thread(existing_thread_id).await { + Ok(existing_thread) => Some((existing_thread_id, existing_thread, source_thread)), + Err(_) => None, + } }; if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread { @@ -2812,6 +2774,48 @@ impl ThreadRequestProcessor { active_path.display() ))); } + let config_snapshot = existing_thread.config_snapshot().await; + let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot); + if !mismatch_details.is_empty() { + let has_subscribers = !self + .thread_state_manager + .subscribed_connection_ids(existing_thread_id) + .await + .is_empty(); + let loaded_status = self + .thread_watch_manager + .loaded_status_for_thread(&existing_thread_id.to_string()) + .await; + let is_running = + matches!(existing_thread.agent_status().await, AgentStatus::Running); + + if !has_subscribers && matches!(loaded_status, ThreadStatus::Idle) && !is_running { + // A loaded idle thread is only a cache entry. Shut it down + // before removing it so cold resume cannot duplicate a + // thread that timed out during shutdown. + match wait_for_thread_shutdown(&existing_thread).await { + ThreadShutdownResult::Complete => { + self.thread_manager.remove_thread(&existing_thread_id).await; + self.finalize_thread_teardown(existing_thread_id).await; + return Ok(false); + } + ThreadShutdownResult::SubmitFailed => { + warn!("failed to submit Shutdown to thread {existing_thread_id}"); + } + ThreadShutdownResult::TimedOut => { + warn!("thread {existing_thread_id} shutdown timed out"); + } + } + } + + // Preserve rejoin semantics when another client can still observe + // the loaded thread or shutdown did not complete. + tracing::warn!( + "thread/resume overrides ignored for loaded thread {}: {}", + existing_thread_id, + mismatch_details.join("; ") + ); + } let redact_resume_payloads = should_redact_thread_resume_payloads(app_server_client_name.as_deref()); let history_items = source_thread @@ -2841,15 +2845,6 @@ impl ThreadRequestProcessor { ) .await?; - let config_snapshot = existing_thread.config_snapshot().await; - let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot); - if !mismatch_details.is_empty() { - tracing::warn!( - "thread/resume overrides ignored for running thread {}: {}", - existing_thread_id, - mismatch_details.join("; ") - ); - } let mut summary_source_thread = source_thread; summary_source_thread.history = None; let mut thread_summary = self.stored_thread_to_api_thread( @@ -2888,6 +2883,7 @@ impl ThreadRequestProcessor { emit_thread_goal_update, thread_goal_state_db, include_turns: !params.exclude_turns, + initial_turns_page: params.initial_turns_page.clone(), redact_resume_payloads, }), ); @@ -2949,7 +2945,7 @@ impl ThreadRequestProcessor { let existing_thread_id = match ThreadId::from_string(thread_id) { Ok(id) => id, Err(err) => { - return Err(invalid_request(format!("invalid thread id: {err}"))); + return Err(invalid_request(format!("invalid session id: {err}"))); } }; let params = StoreReadThreadParams { @@ -2960,7 +2956,15 @@ impl ThreadRequestProcessor { self.thread_store.read_thread(params).await }; - result.map_err(thread_store_resume_read_error) + let stored_thread = result.map_err(thread_store_resume_read_error)?; + if stored_thread.archived_at.is_some() { + let thread_id = stored_thread.thread_id; + return Err(invalid_request(format!( + "session {thread_id} is archived. Run `codex unarchive {thread_id}` to unarchive it first." + ))); + } + + Ok(stored_thread) } async fn stored_thread_to_initial_history( @@ -3158,7 +3162,6 @@ impl ThreadRequestProcessor { ephemeral, thread_source, exclude_turns, - persist_extended_history, } = params; let include_turns = !exclude_turns; if sandbox.is_some() && permissions.is_some() { @@ -3166,11 +3169,6 @@ impl ThreadRequestProcessor { "`permissions` cannot be combined with `sandbox`", )); } - if persist_extended_history { - self.send_persist_extended_history_deprecation_notice(request_id.connection_id) - .await; - } - let source_thread = self .read_stored_thread_for_resume(&thread_id, path.as_ref(), /*include_history*/ true) .await?; @@ -3249,7 +3247,6 @@ impl ThreadRequestProcessor { rollout_path: source_thread.rollout_path.clone(), }), thread_source.map(Into::into), - /*persist_extended_history*/ false, self.request_trace_context(&request_id).await, ) .await @@ -3668,6 +3665,95 @@ fn parse_thread_turns_cursor(cursor: &str) -> Result { + cursor: Option<&'a str>, + limit: Option, + sort_direction: SortDirection, + items_view: TurnItemsView, +} + +fn build_thread_turns_page_response( + items: &[RolloutItem], + loaded_status: ThreadStatus, + has_live_running_thread: bool, + active_turn: Option, + options: ThreadTurnsPageOptions<'_>, +) -> Result { + let mut turns = reconstruct_thread_turns_for_turns_list( + items, + loaded_status, + has_live_running_thread, + active_turn, + ); + apply_thread_turns_items_view(&mut turns, options.items_view); + let page = paginate_thread_turns(turns, options.cursor, options.limit, options.sort_direction)?; + Ok(ThreadTurnsListResponse { + data: page.turns, + next_cursor: page.next_cursor, + backwards_cursor: page.backwards_cursor, + }) +} + +pub(super) fn build_thread_resume_initial_turns_page( + items: &[RolloutItem], + loaded_status: ThreadStatus, + has_live_running_thread: bool, + active_turn: Option, + params: &ThreadResumeInitialTurnsPageParams, +) -> Result { + build_thread_turns_page_response( + items, + loaded_status, + has_live_running_thread, + active_turn, + ThreadTurnsPageOptions { + cursor: None, + limit: params.limit, + sort_direction: params.sort_direction.unwrap_or(SortDirection::Desc), + items_view: params.items_view.unwrap_or(TurnItemsView::Summary), + }, + ) + .map(Into::into) +} + +fn apply_thread_turns_items_view(turns: &mut [Turn], items_view: TurnItemsView) { + for turn in turns { + match items_view { + TurnItemsView::NotLoaded => { + turn.items.clear(); + turn.items_view = TurnItemsView::NotLoaded; + } + TurnItemsView::Summary => { + let first_user_message = turn + .items + .iter() + .find(|item| matches!(item, ThreadItem::UserMessage { .. })) + .cloned(); + let final_agent_message = turn + .items + .iter() + .rev() + .find(|item| matches!(item, ThreadItem::AgentMessage { .. })) + .cloned(); + turn.items = match (first_user_message, final_agent_message) { + (Some(user_message), Some(agent_message)) + if user_message.id() != agent_message.id() => + { + vec![user_message, agent_message] + } + (Some(user_message), _) => vec![user_message], + (None, Some(agent_message)) => vec![agent_message], + (None, None) => Vec::new(), + }; + turn.items_view = TurnItemsView::Summary; + } + TurnItemsView::Full => { + turn.items_view = TurnItemsView::Full; + } + } + } +} + fn reconstruct_thread_turns_for_turns_list( items: &[RolloutItem], loaded_status: ThreadStatus, @@ -3856,7 +3942,7 @@ fn thread_store_archive_error(operation: &str, err: ThreadStoreError) -> JSONRPC ThreadStoreError::Unsupported { operation: unsupported_operation, } => unsupported_thread_store_operation(unsupported_operation), - err => internal_error(format!("failed to {operation} thread: {err}")), + err => internal_error(format!("failed to {operation} session: {err}")), } } @@ -3896,6 +3982,7 @@ pub(crate) fn thread_from_stored_thread( id: thread_id.clone(), session_id: thread_id, forked_from_id: thread.forked_from_id.map(|id| id.to_string()), + parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()), preview: thread.preview, ephemeral: false, model_provider: if thread.model_provider.is_empty() { @@ -4104,6 +4191,7 @@ fn build_thread_from_snapshot( id: thread_id.to_string(), session_id, forked_from_id: None, + parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()), preview: String::new(), ephemeral: config_snapshot.ephemeral, model_provider: config_snapshot.model_provider_id.clone(), diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index ab8f52ffc0..8689193ced 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -68,13 +68,13 @@ mod thread_processor_behavior_tests { use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; + use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_state::ThreadMetadataBuilder; @@ -213,6 +213,7 @@ mod thread_processor_behavior_tests { fn thread_turns_list_merges_in_progress_active_turn_before_agent_status_running() { let persisted_items = vec![RolloutItem::EventMsg(EventMsg::UserMessage( codex_protocol::protocol::UserMessageEvent { + client_id: None, message: "persisted".to_string(), images: None, local_images: Vec::new(), @@ -224,6 +225,7 @@ mod thread_processor_behavior_tests { id: "live-turn".to_string(), items: vec![ThreadItem::UserMessage { id: "live-user-message".to_string(), + client_id: None, content: vec![V2UserInput::Text { text: "live".to_string(), text_elements: Vec::new(), @@ -393,6 +395,7 @@ mod thread_processor_behavior_tests { thread_id, rollout_path: Some(PathBuf::from("/tmp/thread.jsonl")), forked_from_id: None, + parent_thread_id: None, preview: "preview".to_string(), name: None, model_provider: "openai".to_string(), @@ -410,7 +413,7 @@ mod thread_processor_behavior_tests { agent_path: None, git_info: None, approval_mode: AskForApproval::OnRequest, - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: PermissionProfile::read_only(), token_usage: None, first_user_message: Some("first user message".to_string()), history: None, @@ -652,7 +655,7 @@ mod thread_processor_behavior_tests { developer_instructions: None, personality: None, exclude_turns: false, - persist_extended_history: false, + initial_turns_page: None, }; let config_snapshot = ThreadConfigSnapshot { model: "gpt-5".to_string(), @@ -678,6 +681,7 @@ mod thread_processor_behavior_tests { }, }, session_source: SessionSource::Cli, + parent_thread_id: None, thread_source: None, }; diff --git a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs index b1e8608842..e970a88a9f 100644 --- a/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs +++ b/codex-rs/app-server/src/request_processors/thread_resume_redaction.rs @@ -1,6 +1,6 @@ use codex_app_server_protocol::McpToolCallResult; -use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::Turn; use serde_json::Value as JsonValue; // Temporary bandaid for remote clients: thread/resume can include large MCP and @@ -14,8 +14,8 @@ pub(super) fn should_redact_thread_resume_payloads(client_name: Option<&str>) -> client_name.is_some_and(|client_name| CHATGPT_REMOTE_CLIENT_NAMES.contains(&client_name)) } -pub(super) fn redact_thread_resume_payloads(thread: &mut Thread) { - for turn in &mut thread.turns { +pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) { + for turn in turns { turn.items.retain_mut(|item| match item { ThreadItem::McpToolCall { arguments, @@ -55,8 +55,8 @@ mod tests { use codex_app_server_protocol::McpToolCallError; use codex_app_server_protocol::McpToolCallStatus; use codex_app_server_protocol::SessionSource; + use codex_app_server_protocol::Thread; use codex_app_server_protocol::ThreadStatus; - use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; use codex_utils_absolute_path::test_support::PathBufExt; @@ -100,7 +100,7 @@ mod tests { }, ]); - redact_thread_resume_payloads(&mut thread); + redact_thread_resume_payloads(&mut thread.turns); assert_eq!(thread.turns[0].items.len(), 2); assert_eq!( @@ -146,7 +146,7 @@ mod tests { duration_ms: Some(8), }]); - redact_thread_resume_payloads(&mut thread); + redact_thread_resume_payloads(&mut thread.turns); assert_eq!( thread.turns[0].items[0], @@ -172,6 +172,7 @@ mod tests { id: "thread-1".to_string(), session_id: "session-1".to_string(), forked_from_id: None, + parent_thread_id: None, preview: "preview".to_string(), ephemeral: false, model_provider: "mock_provider".to_string(), diff --git a/codex-rs/app-server/src/request_processors/thread_summary.rs b/codex-rs/app-server/src/request_processors/thread_summary.rs index 0a6c1bfb0f..da7ae54804 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -305,6 +305,7 @@ pub(crate) fn summary_to_thread( id: thread_id.clone(), session_id: thread_id, forked_from_id: None, + parent_thread_id: None, preview, ephemeral: false, model_provider, diff --git a/codex-rs/app-server/src/request_processors/token_usage_replay.rs b/codex-rs/app-server/src/request_processors/token_usage_replay.rs index b8c65645fc..d4d2228d4b 100644 --- a/codex-rs/app-server/src/request_processors/token_usage_replay.rs +++ b/codex-rs/app-server/src/request_processors/token_usage_replay.rs @@ -148,6 +148,7 @@ mod tests { fn token_usage_history() -> Vec { vec![ RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "first turn".to_string(), images: None, local_images: Vec::new(), @@ -164,6 +165,7 @@ mod tests { rate_limits: None, })), RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { + client_id: None, message: "second turn".to_string(), images: None, local_images: Vec::new(), diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index ff2825be67..25beee3cdb 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -1,4 +1,6 @@ use super::*; +use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry; +use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind; #[derive(Clone)] pub(crate) struct TurnRequestProcessor { @@ -30,6 +32,29 @@ fn resolve_runtime_workspace_roots( resolved_roots } +fn map_additional_context( + additional_context: Option>, +) -> BTreeMap { + additional_context + .unwrap_or_default() + .into_iter() + .map(|(key, entry)| { + ( + key, + CoreAdditionalContextEntry { + value: entry.value, + kind: match entry.kind { + AdditionalContextKind::Untrusted => CoreAdditionalContextKind::Untrusted, + AdditionalContextKind::Application => { + CoreAdditionalContextKind::Application + } + }, + }, + ) + }) + .collect() +} + struct ThreadSettingsBuildParams { method: &'static str, cwd: Option, @@ -391,6 +416,8 @@ impl TurnRequestProcessor { .into_iter() .map(V2UserInput::into_core) .collect(); + let client_user_message_id = params.client_user_message_id; + let additional_context = map_additional_context(params.additional_context); let turn_has_input = !mapped_items.is_empty(); let thread_settings = self .build_thread_settings_overrides( @@ -419,10 +446,15 @@ impl TurnRequestProcessor { environments: environment_selections, final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, + additional_context, thread_settings, }; - let turn_id = self - .submit_core_op(&request_id, thread.as_ref(), turn_op) + let turn_id = thread + .submit_user_input_with_client_user_message_id( + turn_op, + self.request_trace_context(&request_id).await, + client_user_message_id, + ) .await .map_err(|err| { let error = internal_error(format!("failed to start turn: {err}")); @@ -746,11 +778,14 @@ impl TurnRequestProcessor { .into_iter() .map(V2UserInput::into_core) .collect(); + let additional_context = map_additional_context(params.additional_context); let turn_id = thread .steer_input( mapped_items, + additional_context, Some(¶ms.expected_turn_id), + params.client_user_message_id, params.responsesapi_client_metadata, ) .await @@ -960,6 +995,7 @@ impl TurnRequestProcessor { } else { vec![ThreadItem::UserMessage { id: turn_id.clone(), + client_id: None, content: vec![V2UserInput::Text { text: display_text.to_string(), // Review prompt display text is synthesized; no UI element ranges to preserve. @@ -1060,7 +1096,6 @@ impl TurnRequestProcessor { rollout_path: parent_thread.rollout_path(), }), /*thread_source*/ None, - /*persist_extended_history*/ false, self.request_trace_context(request_id).await, ) .await diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 0b639b7d5c..c0917d933b 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -41,6 +41,29 @@ impl WindowsSandboxRequestProcessor { request_id: &ConnectionRequestId, params: WindowsSandboxSetupStartParams, ) -> Result<(), JSONRPCErrorError> { + // Validate requirements before acknowledging setup so callers do not get a + // `started` response for a Windows sandbox mode that cannot be persisted. + let command_cwd = params + .cwd + .map(PathBuf::from) + .unwrap_or_else(|| self.config.cwd.to_path_buf()); + let config = self + .config_manager + .load_for_cwd( + /*request_overrides*/ None, + ConfigOverrides { + cwd: Some(command_cwd.clone()), + ..Default::default() + }, + Some(command_cwd.clone()), + ) + .await + .map_err(|err| config_load_error(&err))?; + let setup_mode = resolve_allowed_windows_sandbox_setup_mode( + config.config_layer_stack.requirements(), + params.mode, + )?; + self.outgoing .send_response( request_id.clone(), @@ -48,48 +71,22 @@ impl WindowsSandboxRequestProcessor { ) .await; - let mode = match params.mode { - WindowsSandboxSetupMode::Elevated => CoreWindowsSandboxSetupMode::Elevated, - WindowsSandboxSetupMode::Unelevated => CoreWindowsSandboxSetupMode::Unelevated, - }; - let config = Arc::clone(&self.config); - let config_manager = self.config_manager.clone(); - let command_cwd = params - .cwd - .map(PathBuf::from) - .unwrap_or_else(|| config.cwd.to_path_buf()); let outgoing = Arc::clone(&self.outgoing); let connection_id = request_id.connection_id; tokio::spawn(async move { - let derived_config = config_manager - .load_for_cwd( - /*request_overrides*/ None, - ConfigOverrides { - cwd: Some(command_cwd.clone()), - ..Default::default() - }, - Some(command_cwd.clone()), - ) - .await; - let setup_result = match derived_config { - Ok(config) => { - let setup_request = WindowsSandboxSetupRequest { - mode, - policy: config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), - policy_cwd: config.cwd.to_path_buf(), - command_cwd, - env_map: std::env::vars().collect(), - codex_home: config.codex_home.to_path_buf(), - }; - codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await - } - Err(err) => Err(err.into()), + let setup_request = WindowsSandboxSetupRequest { + mode: setup_mode, + permission_profile: config.permissions.effective_permission_profile(), + workspace_roots: config.effective_workspace_roots(), + command_cwd, + env_map: std::env::vars().collect(), + codex_home: config.codex_home.to_path_buf(), }; + let setup_result = + codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await; let notification = WindowsSandboxSetupCompletedNotification { - mode: match mode { + mode: match setup_mode { CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated, CoreWindowsSandboxSetupMode::Unelevated => WindowsSandboxSetupMode::Unelevated, }, @@ -107,6 +104,28 @@ impl WindowsSandboxRequestProcessor { } } +/// Resolves the requested API mode after checking that managed requirements allow it. +fn resolve_allowed_windows_sandbox_setup_mode( + requirements: &codex_config::ConfigRequirements, + requested_mode: WindowsSandboxSetupMode, +) -> Result { + let (setup_mode, config_mode) = match requested_mode { + WindowsSandboxSetupMode::Elevated => ( + CoreWindowsSandboxSetupMode::Elevated, + codex_config::types::WindowsSandboxModeToml::Elevated, + ), + WindowsSandboxSetupMode::Unelevated => ( + CoreWindowsSandboxSetupMode::Unelevated, + codex_config::types::WindowsSandboxModeToml::Unelevated, + ), + }; + requirements + .windows_sandbox_mode + .can_set(&Some(config_mode)) + .map_err(|err| invalid_request(format!("invalid Windows sandbox setup mode: {err}")))?; + Ok(setup_mode) +} + fn determine_windows_sandbox_readiness(config: &Config) -> WindowsSandboxReadinessResponse { if !cfg!(windows) { return WindowsSandboxReadinessResponse { @@ -142,6 +161,34 @@ fn determine_windows_sandbox_readiness_from_state( #[cfg(test)] mod tests { use super::*; + use crate::error_code::INVALID_REQUEST_ERROR_CODE; + use codex_config::ConfigRequirements; + use codex_config::Constrained; + use codex_config::ConstrainedWithSource; + use codex_config::types::WindowsSandboxModeToml; + + #[test] + fn resolve_allowed_windows_sandbox_setup_mode_rejects_disallowed_mode() { + let requirements = ConfigRequirements { + windows_sandbox_mode: ConstrainedWithSource::new( + Constrained::allow_only(Some(WindowsSandboxModeToml::Elevated)), + /*source*/ None, + ), + ..Default::default() + }; + + let err = resolve_allowed_windows_sandbox_setup_mode( + &requirements, + WindowsSandboxSetupMode::Unelevated, + ) + .expect_err("unelevated setup should be rejected"); + + assert_eq!(err.code, INVALID_REQUEST_ERROR_CODE); + assert!( + err.message.contains("invalid Windows sandbox setup mode"), + "{err:?}" + ); + } #[test] fn determine_windows_sandbox_readiness_reports_not_configured_when_disabled() { diff --git a/codex-rs/app-server/src/skills_watcher.rs b/codex-rs/app-server/src/skills_watcher.rs index 9e34cdac8e..57edb89c7f 100644 --- a/codex-rs/app-server/src/skills_watcher.rs +++ b/codex-rs/app-server/src/skills_watcher.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use crate::outgoing_message::OutgoingMessageSender; @@ -15,6 +16,7 @@ use codex_file_watcher::ThrottledWatchReceiver; use codex_file_watcher::WatchPath; use codex_file_watcher::WatchRegistration; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_utils_absolute_path::AbsolutePathBuf; use tokio_util::sync::CancellationToken; use tokio_util::sync::DropGuard; use tracing::warn; @@ -26,6 +28,7 @@ const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_millis(50); pub(crate) struct SkillsWatcher { subscriber: FileWatcherSubscriber, + runtime_extra_roots_registration: Mutex, shutdown_token: CancellationToken, _shutdown_drop_guard: DropGuard, } @@ -48,6 +51,7 @@ impl SkillsWatcher { Self::spawn_event_loop(rx, skills_manager, outgoing, shutdown_token.child_token()); Arc::new(Self { subscriber, + runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()), shutdown_token, _shutdown_drop_guard: shutdown_drop_guard, }) @@ -57,6 +61,22 @@ impl SkillsWatcher { self.shutdown_token.cancel(); } + pub(crate) fn register_runtime_extra_roots(&self, extra_roots: &[AbsolutePathBuf]) { + let roots = extra_roots + .iter() + .map(|root| WatchPath { + path: root.clone().into_path_buf(), + recursive: true, + }) + .collect(); + let registration = self.subscriber.register_paths(roots); + let mut guard = self + .runtime_extra_roots_registration + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *guard = registration; + } + pub(crate) async fn register_thread_config( &self, config: &Config, diff --git a/codex-rs/app-server/src/thread_state.rs b/codex-rs/app-server/src/thread_state.rs index 7f864265b2..2d932f0326 100644 --- a/codex-rs/app-server/src/thread_state.rs +++ b/codex-rs/app-server/src/thread_state.rs @@ -35,6 +35,8 @@ pub(crate) struct PendingThreadResumeRequest { pub(crate) emit_thread_goal_update: bool, pub(crate) thread_goal_state_db: Option, pub(crate) include_turns: bool, + pub(crate) initial_turns_page: + Option, pub(crate) redact_resume_payloads: bool, } diff --git a/codex-rs/app-server/src/thread_status.rs b/codex-rs/app-server/src/thread_status.rs index 7315a13c02..6d66a32d6a 100644 --- a/codex-rs/app-server/src/thread_status.rs +++ b/codex-rs/app-server/src/thread_status.rs @@ -891,6 +891,7 @@ mod tests { id: thread_id.to_string(), session_id: thread_id.to_string(), forked_from_id: None, + parent_thread_id: None, preview: String::new(), ephemeral: false, model_provider: "mock-provider".to_string(), diff --git a/codex-rs/app-server/tests/common/lib.rs b/codex-rs/app-server/tests/common/lib.rs index 6bb600bd82..2285907ad9 100644 --- a/codex-rs/app-server/tests/common/lib.rs +++ b/codex-rs/app-server/tests/common/lib.rs @@ -1,11 +1,11 @@ mod analytics_server; mod auth_fixtures; mod config; -mod mcp_process; mod mock_model_server; mod models_cache; mod responses; mod rollout; +mod test_app_server; pub use analytics_server::start_analytics_events_server; pub use auth_fixtures::ChatGptAuthFixture; @@ -24,9 +24,6 @@ pub use core_test_support::test_absolute_path; pub use core_test_support::test_path_buf_with_windows; pub use core_test_support::test_tmp_path; pub use core_test_support::test_tmp_path_buf; -pub use mcp_process::DEFAULT_CLIENT_NAME; -pub use mcp_process::DISABLE_PLUGIN_STARTUP_TASKS_ARG; -pub use mcp_process::McpProcess; pub use mock_model_server::create_mock_responses_server_repeating_assistant; pub use mock_model_server::create_mock_responses_server_sequence; pub use mock_model_server::create_mock_responses_server_sequence_unchecked; @@ -38,12 +35,16 @@ pub use responses::create_final_assistant_message_sse_response; pub use responses::create_request_permissions_sse_response; pub use responses::create_request_user_input_sse_response; pub use responses::create_shell_command_sse_response; +pub use rollout::create_fake_parented_rollout_with_source; pub use rollout::create_fake_rollout; pub use rollout::create_fake_rollout_with_source; pub use rollout::create_fake_rollout_with_text_elements; pub use rollout::create_fake_rollout_with_token_usage; pub use rollout::rollout_path; use serde::de::DeserializeOwned; +pub use test_app_server::DEFAULT_CLIENT_NAME; +pub use test_app_server::DISABLE_PLUGIN_STARTUP_TASKS_ARG; +pub use test_app_server::TestAppServer; pub fn to_response(response: JSONRPCResponse) -> anyhow::Result { let value = serde_json::to_value(response.result)?; diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 0a41daa7c6..56a9378662 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -52,6 +52,8 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { input_modalities: default_input_modalities(), used_fallback_model_metadata: false, supports_search_tool: false, + auto_review_model_override: None, + tool_mode: None, } } diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 6b2a9a0abe..63681382ea 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -118,6 +118,53 @@ pub fn create_fake_rollout_with_source( model_provider: Option<&str>, git_info: Option, source: SessionSource, +) -> Result { + create_fake_rollout_with_source_and_parent_thread_id( + codex_home, + filename_ts, + meta_rfc3339, + preview, + model_provider, + git_info, + source, + /*parent_thread_id*/ None, + ) +} + +/// Create a minimal rollout file with an explicit session source and control parent. +#[allow(clippy::too_many_arguments)] +pub fn create_fake_parented_rollout_with_source( + codex_home: &Path, + filename_ts: &str, + meta_rfc3339: &str, + preview: &str, + model_provider: Option<&str>, + git_info: Option, + source: SessionSource, + parent_thread_id: ThreadId, +) -> Result { + create_fake_rollout_with_source_and_parent_thread_id( + codex_home, + filename_ts, + meta_rfc3339, + preview, + model_provider, + git_info, + source, + Some(parent_thread_id), + ) +} + +#[allow(clippy::too_many_arguments)] +fn create_fake_rollout_with_source_and_parent_thread_id( + codex_home: &Path, + filename_ts: &str, + meta_rfc3339: &str, + preview: &str, + model_provider: Option<&str>, + git_info: Option, + source: SessionSource, + parent_thread_id: Option, ) -> Result { let uuid = Uuid::new_v4(); let uuid_str = uuid.to_string(); @@ -133,6 +180,7 @@ pub fn create_fake_rollout_with_source( let meta = SessionMeta { id: conversation_id, forked_from_id: None, + parent_thread_id, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), originator: "codex".to_string(), @@ -217,6 +265,7 @@ pub fn create_fake_rollout_with_text_elements( let meta = SessionMeta { id: conversation_id, forked_from_id: None, + parent_thread_id: None, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), originator: "codex".to_string(), diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/test_app_server.rs similarity index 99% rename from codex-rs/app-server/tests/common/mcp_process.rs rename to codex-rs/app-server/tests/common/test_app_server.rs index 2d2c26b5a6..3a3787d234 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -71,6 +71,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewStartParams; use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SkillsExtraRootsSetParams; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadCompactStartParams; @@ -105,7 +106,7 @@ use codex_app_server_protocol::WindowsSandboxSetupStartParams; use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR; use tokio::process::Command; -pub struct McpProcess { +pub struct TestAppServer { next_request_id: AtomicI64, /// Retain this child process until the client is dropped. The Tokio runtime /// will make a "best effort" to reap the process after it exits, but it is @@ -121,7 +122,7 @@ pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests"; pub const DISABLE_PLUGIN_STARTUP_TASKS_ARG: &str = "--disable-plugin-startup-tasks-for-tests"; const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG"; -impl McpProcess { +impl TestAppServer { pub async fn new(codex_home: &Path) -> anyhow::Result { Self::new_with_env_and_args(codex_home, &[], &[DISABLE_PLUGIN_STARTUP_TASKS_ARG]).await } @@ -675,6 +676,15 @@ impl McpProcess { self.send_request("skills/list", params).await } + /// Send a `skills/extraRoots/set` JSON-RPC request. + pub async fn send_skills_extra_roots_set_request( + &mut self, + params: SkillsExtraRootsSetParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("skills/extraRoots/set", params).await + } + /// Send a `hooks/list` JSON-RPC request. pub async fn send_hooks_list_request( &mut self, @@ -1503,7 +1513,7 @@ impl McpProcess { } } -impl Drop for McpProcess { +impl Drop for TestAppServer { fn drop(&mut self) { // These tests spawn a `codex-app-server` child process. // diff --git a/codex-rs/app-server/tests/suite/auth.rs b/codex-rs/app-server/tests/suite/auth.rs index 1e60871012..b1ef99cb71 100644 --- a/codex-rs/app-server/tests/suite/auth.rs +++ b/codex-rs/app-server/tests/suite/auth.rs @@ -1,6 +1,6 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use chrono::Duration; @@ -92,7 +92,7 @@ shell_snapshot = false std::fs::write(config_toml, contents) } -async fn login_with_api_key_via_request(mcp: &mut McpProcess, api_key: &str) -> Result<()> { +async fn login_with_api_key_via_request(mcp: &mut TestAppServer, api_key: &str) -> Result<()> { let request_id = mcp.send_login_account_api_key_request(api_key).await?; let resp: JSONRPCResponse = timeout( @@ -110,7 +110,8 @@ async fn get_auth_status_no_auth() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path())?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -136,7 +137,7 @@ async fn get_auth_status_with_api_key() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; @@ -164,7 +165,7 @@ async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_custom_provider(codex_home.path(), /*requires_openai_auth*/ false)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; @@ -197,7 +198,7 @@ async fn get_auth_status_with_api_key_no_include_token() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; @@ -225,7 +226,7 @@ async fn get_auth_status_with_api_key_refresh_requested() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key_via_request(&mut mcp, "sk-test-key").await?; @@ -281,7 +282,7 @@ async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result .await; let refresh_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -363,7 +364,7 @@ async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result .await; let refresh_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -430,7 +431,7 @@ async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Res .await; let refresh_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -507,7 +508,7 @@ async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_forced_login(codex_home.path(), "chatgpt")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/conversation_summary.rs b/codex-rs/app-server/tests/suite/conversation_summary.rs index 5253850107..109999d90e 100644 --- a/codex-rs/app-server/tests/suite/conversation_summary.rs +++ b/codex-rs/app-server/tests/suite/conversation_summary.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_fake_rollout; use app_test_support::rollout_path; use app_test_support::to_response; @@ -26,7 +26,6 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::ThreadMemoryMode; use codex_thread_store::CreateThreadParams; use codex_thread_store::InMemoryThreadStore; -use codex_thread_store::ThreadEventPersistenceMode; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; use codex_utils_absolute_path::AbsolutePathBuf; @@ -93,7 +92,7 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> { ))?, ); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -124,6 +123,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> .create_thread(CreateThreadParams { thread_id, forked_from_id: None, + parent_thread_id: None, source: SessionSource::Cli, thread_source: None, base_instructions: BaseInstructions::default(), @@ -133,7 +133,6 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> model_provider: "test-provider".to_string(), memory_mode: ThreadMemoryMode::Disabled, }, - event_persistence_mode: ThreadEventPersistenceMode::default(), }) .await?; @@ -211,7 +210,7 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h let relative_path = rollout_path.strip_prefix(codex_home.path())?.to_path_buf(); let expected = expected_summary(thread_id, normalized_canonical_path(rollout_path)?); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp 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 f508e0c987..2c30d05fde 100644 --- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs +++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs @@ -1,6 +1,6 @@ use anyhow::Result; use anyhow::anyhow; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification; use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification; use codex_app_server_protocol::JSONRPCResponse; @@ -44,15 +44,15 @@ shell_snapshot = false ) } -async fn initialized_mcp(codex_home: &TempDir) -> Result { +async fn initialized_mcp(codex_home: &TempDir) -> Result { create_config_toml(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; Ok(mcp) } async fn wait_for_session_updated( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, session_id: &str, query: &str, file_expectation: FileExpectation, @@ -99,7 +99,7 @@ async fn wait_for_session_updated( } async fn wait_for_session_completed( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, session_id: &str, ) -> Result { let description = format!("session completion for sessionId={session_id}"); @@ -140,7 +140,7 @@ async fn wait_for_session_completed( } async fn assert_update_request_fails_for_missing_session( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, session_id: &str, query: &str, ) -> Result<()> { @@ -161,7 +161,7 @@ async fn assert_update_request_fails_for_missing_session( } async fn assert_no_session_updates_for( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, session_id: &str, grace_period: std::time::Duration, duration: std::time::Duration, @@ -236,7 +236,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> { .to_string(); // Start MCP server and initialize. - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let root_path = root.path().to_string_lossy().to_string(); @@ -302,7 +302,7 @@ async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()> { std::fs::write(root.path().join("alpha.txt"), "contents")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let root_path = root.path().to_string_lossy().to_string(); diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index a15b46abd9..1b95bc7d5b 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -1,6 +1,6 @@ use anyhow::Result; use anyhow::bail; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::ChatGptAuthFixture; @@ -201,7 +201,8 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> { )?; assert!(codex_home.path().join("auth.json").exists()); - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let id = mcp.send_logout_account_request().await?; @@ -268,7 +269,8 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_EMBEDDED), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -342,7 +344,8 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_EMBEDDED), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -401,7 +404,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> { } async fn respond_to_refresh_request( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, access_token: &str, chatgpt_account_id: &str, chatgpt_plan_type: Option<&str>, @@ -467,7 +470,8 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_REFRESHED), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -506,6 +510,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> { let turn_req = mcp .send_turn_start_request(codex_app_server_protocol::TurnStartParams { thread_id: thread.thread.id, + client_user_message_id: None, input: vec![codex_app_server_protocol::UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -573,7 +578,8 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_INITIAL), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -612,6 +618,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> { let turn_req = mcp .send_turn_start_request(codex_app_server_protocol::TurnStartParams { thread_id: thread.thread.id.clone(), + client_user_message_id: None, input: vec![codex_app_server_protocol::UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -695,7 +702,8 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_DISALLOWED), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -734,6 +742,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> { let turn_req = mcp .send_turn_start_request(codex_app_server_protocol::TurnStartParams { thread_id: thread.thread.id.clone(), + client_user_message_id: None, input: vec![codex_app_server_protocol::UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -810,7 +819,8 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> { .chatgpt_account_id(WORKSPACE_ID_INITIAL), )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let set_id = mcp @@ -849,6 +859,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> { let turn_req = mcp .send_turn_start_request(codex_app_server_protocol::TurnStartParams { thread_id: thread.thread.id.clone(), + client_user_message_id: None, input: vec![codex_app_server_protocol::UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -902,7 +913,7 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -956,7 +967,7 @@ async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> { }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -986,7 +997,7 @@ async fn login_account_chatgpt_rejected_when_forced_api() -> Result<()> { }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_login_account_chatgpt_request().await?; @@ -1019,7 +1030,7 @@ async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Resu mock_device_code_usercode_failure(&mock_server, /*status*/ 404).await; let issuer = mock_server.uri(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -1084,7 +1095,7 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()> mock_device_code_oauth_token(&mock_server, &id_token).await; let issuer = mock_server.uri(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -1161,7 +1172,7 @@ async fn login_account_chatgpt_device_code_failure_notifies_without_account_upda mock_device_code_token_failure(&mock_server, /*status*/ 500).await; let issuer = mock_server.uri(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -1236,7 +1247,7 @@ async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()> { mock_device_code_token_failure(&mock_server, /*status*/ 404).await; let issuer = mock_server.uri(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -1309,7 +1320,7 @@ async fn login_account_chatgpt_start_can_be_cancelled() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_login_account_chatgpt_request().await?; @@ -1375,7 +1386,7 @@ async fn set_auth_token_cancels_active_chatgpt_login() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Initiate the ChatGPT login flow @@ -1450,7 +1461,7 @@ async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_login_account_chatgpt_request().await?; @@ -1487,7 +1498,7 @@ async fn login_account_chatgpt_includes_forced_workspace_allowlist_query_param() }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_login_account_chatgpt_request().await?; @@ -1526,7 +1537,8 @@ async fn get_account_no_auth() -> Result<()> { }, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let params = GetAccountParams { @@ -1557,7 +1569,7 @@ async fn get_account_with_api_key() -> Result<()> { }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -1601,7 +1613,7 @@ async fn get_account_when_auth_not_required() -> Result<()> { }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let params = GetAccountParams { @@ -1642,7 +1654,7 @@ region = "us-west-2" }, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let params = GetAccountParams { @@ -1683,7 +1695,8 @@ async fn get_account_with_chatgpt() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let params = GetAccountParams { @@ -1743,7 +1756,7 @@ async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<( .await; let refresh_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -1809,7 +1822,8 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let params = GetAccountParams { diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index c6f95af95d..8e8f2a7557 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -168,11 +168,13 @@ pub(crate) fn thread_initialized_event(payload: &Value) -> Result<&Value> { pub(crate) fn assert_basic_thread_initialized_event( event: &Value, thread_id: &str, + session_id: &str, expected_model: &str, initialization_mode: &str, expected_thread_source: &str, ) { assert_eq!(event["event_params"]["thread_id"], thread_id); + assert_eq!(event["event_params"]["session_id"], session_id); assert_eq!( event["event_params"]["app_server_client"]["product_client_id"], DEFAULT_CLIENT_NAME diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index 395dff5668..ea7ff8fa4b 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -8,7 +8,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::bail; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use axum::Json; @@ -63,7 +63,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); #[tokio::test] async fn list_apps_returns_empty_when_connectors_disabled() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -124,7 +124,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -186,7 +186,7 @@ async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Resu AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -245,7 +245,7 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let start_request = mcp @@ -316,6 +316,73 @@ connectors = false Ok(()) } +#[tokio::test] +async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()> { + let connectors = vec![AppInfo { + id: "beta".to_string(), + name: "Beta".to_string(), + description: Some("Beta connector".to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + }]; + let mut app_only_tool = connector_tool("beta", "Beta App")?; + app_only_tool + .meta + .as_mut() + .expect("connector tool should include metadata") + .0 + .insert("ui".to_string(), json!({ "visibility": ["app"] })); + let tools = vec![app_only_tool]; + let (server_url, server_handle) = + start_apps_server_with_delays(connectors, tools, Duration::ZERO, Duration::ZERO).await?; + + let codex_home = TempDir::new()?; + write_connectors_config(codex_home.path(), &server_url)?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .chatgpt_user_id("user-app-only") + .chatgpt_account_id("account-123"), + AuthCredentialsStoreMode::File, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_apps_list_request(AppsListParams { + limit: None, + cursor: None, + thread_id: None, + force_refetch: true, + }) + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let AppsListResponse { data, next_cursor } = to_response(response)?; + + assert_eq!(data.len(), 1); + assert_eq!(data[0].id, "beta"); + assert!(data[0].is_accessible); + assert!(next_cursor.is_none()); + + server_handle.abort(); + let _ = server_handle.await; + Ok(()) +} + #[tokio::test] async fn list_apps_reports_is_enabled_from_config() -> Result<()> { let connectors = vec![AppInfo { @@ -361,7 +428,7 @@ enabled = false AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -480,7 +547,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -620,7 +687,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -726,7 +793,7 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -835,7 +902,7 @@ async fn list_apps_paginates_results() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_request = mcp @@ -955,7 +1022,7 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let initial_request = mcp @@ -1081,7 +1148,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let warm_request = mcp @@ -1304,7 +1371,7 @@ async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_ AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let disable_request = mcp @@ -1371,7 +1438,7 @@ async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_ } async fn read_app_list_updated_notification( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let notification = timeout( DEFAULT_TIMEOUT, @@ -1432,10 +1499,7 @@ impl AppsServerControl { impl ServerHandler for AppListMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) } fn list_tools( diff --git a/codex-rs/app-server/tests/suite/v2/attestation.rs b/codex-rs/app-server/tests/suite/v2/attestation.rs index d0565e2571..567f37397a 100644 --- a/codex-rs/app-server/tests/suite/v2/attestation.rs +++ b/codex-rs/app-server/tests/suite/v2/attestation.rs @@ -1,7 +1,7 @@ use anyhow::Result; use anyhow::bail; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::AttestationGenerateResponse; @@ -76,7 +76,8 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; let initialized = timeout( DEFAULT_READ_TIMEOUT, mcp.initialize_with_capabilities( @@ -110,6 +111,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand let turn_request_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/client_metadata.rs b/codex-rs/app-server/tests/suite/v2/client_metadata.rs index 8d68888e7e..873a1cc044 100644 --- a/codex-rs/app-server/tests/suite/v2/client_metadata.rs +++ b/codex-rs/app-server/tests/suite/v2/client_metadata.rs @@ -1,8 +1,20 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; +use app_test_support::create_fake_parented_rollout_with_source; +use app_test_support::create_fake_rollout; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ReviewDelivery; +use codex_app_server_protocol::ReviewStartParams; +use codex_app_server_protocol::ReviewStartResponse; +use codex_app_server_protocol::ReviewTarget; +use codex_app_server_protocol::SessionSource as ApiSessionSource; +use codex_app_server_protocol::ThreadForkParams; +use codex_app_server_protocol::ThreadForkResponse; +use codex_app_server_protocol::ThreadResumeParams; +use codex_app_server_protocol::ThreadResumeResponse; +use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -10,6 +22,9 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput as V2UserInput; +use codex_protocol::ThreadId as CoreThreadId; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; @@ -44,7 +59,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result /*supports_websockets*/ false, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -60,10 +75,12 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result let client_metadata = HashMap::from([ ("fiber_run_id".to_string(), "fiber-start-123".to_string()), ("origin".to_string(), "gaas".to_string()), + ("thread_source".to_string(), "client-supplied".to_string()), ]); let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -93,8 +110,288 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result .unwrap_or_else(|| panic!("missing x-codex-turn-metadata header")); assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123")); assert_eq!(metadata["origin"].as_str(), Some("gaas")); + assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("session_id").is_some()); + assert_eq!( + metadata["window_id"].as_str(), + request.header("x-codex-window-id").as_deref() + ); + + Ok(()) +} + +#[tokio::test] +async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + /*supports_websockets*/ false, + )?; + + let source_thread_id = create_fake_rollout( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Saved user message", + Some("mock_provider"), + /*git_info*/ None, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let ThreadForkResponse { thread, .. } = + fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "Continue".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let metadata = request + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("missing x-codex-turn-metadata header")); + assert_eq!( + metadata["forked_from_thread_id"].as_str(), + Some(source_thread_id.as_str()) + ); + assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str())); + assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); + + Ok(()) +} + +#[tokio::test] +async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> { + skip_if_no_network!(Ok(())); + + let review_payload = serde_json::json!({ + "findings": [], + "overall_correctness": "good", + "overall_explanation": "Done", + "overall_confidence_score": 0.5 + }) + .to_string(); + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", &review_payload), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + /*supports_websockets*/ false, + )?; + + let source_thread_id = create_fake_rollout( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Saved user message", + Some("mock_provider"), + /*git_info*/ None, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let ThreadForkResponse { thread, .. } = + fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?; + + let review_req = mcp + .send_review_start_request(ReviewStartParams { + thread_id: thread.id.clone(), + delivery: Some(ReviewDelivery::Inline), + target: ReviewTarget::Custom { + instructions: "Review the fork".to_string(), + }, + }) + .await?; + let review_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(review_req)), + ) + .await??; + let ReviewStartResponse { + review_thread_id, .. + } = to_response::(review_resp)?; + assert_eq!(review_thread_id, thread.id); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let metadata = request + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("missing x-codex-turn-metadata header")); + assert_eq!( + request.header("x-openai-subagent").as_deref(), + Some("review") + ); + assert!(metadata.get("forked_from_thread_id").is_none()); + assert_eq!( + metadata["parent_thread_id"].as_str(), + Some(review_thread_id.as_str()) + ); + let review_request_thread_id = metadata["thread_id"] + .as_str() + .unwrap_or_else(|| panic!("missing review request thread_id")); + assert!(review_request_thread_id != review_thread_id.as_str()); + assert_eq!( + request + .header("x-codex-window-id") + .as_deref() + .and_then(|window_id| window_id.split_once(':').map(|(thread_id, _)| thread_id)), + Some(review_request_thread_id) + ); + assert!(metadata["turn_id"].as_str().is_some()); + + Ok(()) +} + +#[tokio::test] +async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + /*supports_websockets*/ false, + )?; + + let parent_thread_id = CoreThreadId::new(); + let parent_thread_id_str = parent_thread_id.to_string(); + let subagent_thread_id = create_fake_parented_rollout_with_source( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Saved subagent message", + Some("mock_provider"), + /*git_info*/ None, + SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())), + parent_thread_id, + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_req = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: subagent_thread_id.clone(), + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_req)), + ) + .await??; + let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; + assert_eq!(thread.id, subagent_thread_id); + assert_eq!(thread.parent_thread_id, Some(parent_thread_id_str.clone())); + assert_eq!( + thread.source, + ApiSessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())) + ); + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "Continue".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let request = response_mock.single_request(); + let metadata = request + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("missing x-codex-turn-metadata header")); + assert_eq!( + metadata["parent_thread_id"].as_str(), + Some(parent_thread_id_str.as_str()) + ); + assert_eq!(metadata["subagent_kind"].as_str(), Some("guardian")); + assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str())); + assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); + assert!(metadata.get("forked_from_thread_id").is_none()); Ok(()) } @@ -126,7 +423,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() /*supports_websockets*/ false, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -144,6 +441,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Run sleep".to_string(), text_elements: Vec::new(), @@ -174,11 +472,13 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() let steer_req = mcp .send_turn_steer_request(TurnSteerParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Focus on the failure".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: Some(steer_metadata.clone()), + additional_context: None, expected_turn_id: turn_id.clone(), }) .await?; @@ -248,7 +548,7 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body /*supports_websockets*/ true, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -268,6 +568,7 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -311,6 +612,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body assert_eq!(metadata["origin"].as_str(), Some("gaas")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("session_id").is_some()); + assert_eq!( + metadata["window_id"].as_str(), + request["client_metadata"]["x-codex-window-id"].as_str() + ); websocket_server.shutdown().await; Ok(()) @@ -344,6 +649,25 @@ supports_websockets = {supports_websockets} ) } +async fn fork_fake_rollout_thread( + mcp: &mut TestAppServer, + source_thread_id: String, +) -> Result { + let fork_req = mcp + .send_thread_fork_request(ThreadForkParams { + thread_id: source_thread_id, + thread_source: Some(ThreadSource::User), + ..Default::default() + }) + .await?; + let fork_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(fork_req)), + ) + .await??; + to_response::(fork_resp) +} + fn parse_json_header(value: &str) -> serde_json::Value { match serde_json::from_str(value) { Ok(value) => value, diff --git a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs index f5914f0449..31dd810fbd 100644 --- a/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs +++ b/codex-rs/app-server/tests/suite/v2/collaboration_mode_list.rs @@ -8,7 +8,7 @@ use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::CollaborationModeListParams; use codex_app_server_protocol::CollaborationModeListResponse; @@ -28,7 +28,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); #[tokio::test] async fn list_collaboration_modes_returns_presets() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index 9757767c9f..7cad6c0388 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use base64::Engine; @@ -42,7 +42,7 @@ async fn command_exec_without_streams_can_be_terminated() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let process_id = "sleep-1".to_string(); @@ -92,7 +92,7 @@ async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Resul let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -140,7 +140,7 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[("COMMAND_EXEC_BASELINE", Some("server"))], ) @@ -198,7 +198,7 @@ async fn command_exec_accepts_permission_profile() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -249,7 +249,7 @@ async fn command_exec_permission_profile_starts_selected_network_proxy() -> Resu codex_home.path(), /*default_permissions*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -297,7 +297,7 @@ async fn command_exec_permission_profile_does_not_reuse_default_network_proxy() let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; insert_networked_permission_profile_config(codex_home.path(), Some("networked"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -355,7 +355,7 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu ":workspace_roots" = "write" "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -406,7 +406,7 @@ async fn command_exec_returns_error_when_local_environment_is_disabled() -> Resu let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))], ) @@ -445,7 +445,7 @@ async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -483,7 +483,7 @@ async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -521,7 +521,7 @@ async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Resu let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -559,7 +559,7 @@ async fn command_exec_rejects_negative_timeout_ms() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -597,7 +597,7 @@ async fn command_exec_without_process_id_rejects_streaming() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -635,7 +635,7 @@ async fn command_exec_non_streaming_respects_output_cap() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let command_request_id = mcp @@ -682,7 +682,7 @@ async fn command_exec_streaming_does_not_buffer_output() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let process_id = "stream-cap-1".to_string(); @@ -746,7 +746,7 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let process_id = "pipe-1".to_string(); @@ -822,7 +822,7 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<( let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let process_id = "tty-1".to_string(); @@ -893,7 +893,7 @@ async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let process_id = "tty-size-1".to_string(); @@ -1062,7 +1062,7 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate } async fn read_command_exec_delta( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let notification = mcp .read_stream_until_notification_message("command/exec/outputDelta") @@ -1071,7 +1071,7 @@ async fn read_command_exec_delta( } async fn wait_for_command_exec_output_contains( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, process_id: &str, stream: CommandExecOutputStream, expected: &str, @@ -1094,7 +1094,7 @@ async fn wait_for_command_exec_output_contains( } async fn wait_for_command_exec_outputs_contains( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, process_id: &str, stdout_expected: &str, stderr_expected: &str, @@ -1112,7 +1112,7 @@ async fn wait_for_command_exec_outputs_contains( } enum CommandExecDeltaReader<'a> { - Mcp(&'a mut McpProcess), + Mcp(&'a mut TestAppServer), Websocket(&'a mut super::connection_handling_websocket::WsClient), } diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 6db031b278..690c6d8bff 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -9,7 +9,7 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use app_test_support::write_mock_responses_config_toml; @@ -82,7 +82,7 @@ async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()> COMPACT_PROMPT, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -162,7 +162,8 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -190,6 +191,58 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() let response_requests = responses_log.requests(); assert_eq!(response_requests.len(), 3); + let turn_metadata = response_requests + .iter() + .map(|request| { + request + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("turn request should include turn metadata")) + }) + .collect::>(); + for (request, metadata) in response_requests.iter().zip(&turn_metadata) { + assert_eq!(metadata["request_kind"].as_str(), Some("turn")); + assert!( + metadata["turn_id"] + .as_str() + .is_some_and(|turn_id| !turn_id.is_empty()), + "turn request should carry a non-empty turn id" + ); + assert_eq!( + metadata["window_id"].as_str(), + request.header("x-codex-window-id").as_deref() + ); + assert!(metadata.get("compaction").is_none()); + } + + let compact_metadata = compact_requests[0] + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("compact request should include turn metadata")); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["compaction"], + serde_json::json!({ + "trigger": "auto", + "reason": "context_limit", + "implementation": "responses_compact", + "phase": "pre_turn", + "strategy": "memento", + }) + ); + assert_eq!( + compact_metadata["turn_id"], turn_metadata[2]["turn_id"], + "pre-turn compaction should carry the current turn id" + ); + assert_eq!( + compact_metadata["window_id"].as_str(), + compact_requests[0].header("x-codex-window-id").as_deref() + ); Ok(()) } @@ -216,7 +269,7 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() - COMPACT_PROMPT, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -266,7 +319,7 @@ async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()> { COMPACT_PROMPT, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -302,7 +355,7 @@ async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> { COMPACT_PROMPT, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -322,7 +375,7 @@ async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> { Ok(()) } -async fn start_thread(mcp: &mut McpProcess) -> Result { +async fn start_thread(mcp: &mut TestAppServer) -> Result { let thread_id = mcp .send_thread_start_request(ThreadStartParams { model: Some("mock-model".to_string()), @@ -338,10 +391,15 @@ async fn start_thread(mcp: &mut McpProcess) -> Result { Ok(thread.id) } -async fn send_turn_and_wait(mcp: &mut McpProcess, thread_id: &str, text: &str) -> Result { +async fn send_turn_and_wait( + mcp: &mut TestAppServer, + thread_id: &str, + text: &str, +) -> Result { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.to_string(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: text.to_string(), text_elements: Vec::new(), @@ -359,7 +417,7 @@ async fn send_turn_and_wait(mcp: &mut McpProcess, thread_id: &str, text: &str) - Ok(turn.id) } -async fn wait_for_turn_completed(mcp: &mut McpProcess, turn_id: &str) -> Result<()> { +async fn wait_for_turn_completed(mcp: &mut TestAppServer, turn_id: &str) -> Result<()> { loop { let notification: JSONRPCNotification = timeout( DEFAULT_READ_TIMEOUT, @@ -375,7 +433,7 @@ async fn wait_for_turn_completed(mcp: &mut McpProcess, turn_id: &str) -> Result< } async fn wait_for_context_compaction_started( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { loop { let notification: JSONRPCNotification = timeout( @@ -392,7 +450,7 @@ async fn wait_for_context_compaction_started( } async fn wait_for_context_compaction_completed( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { loop { let notification: JSONRPCNotification = timeout( @@ -407,3 +465,7 @@ async fn wait_for_context_compaction_completed( } } } + +fn parse_json_header(value: &str) -> serde_json::Value { + serde_json::from_str(value).unwrap_or_else(|err| panic!("turn metadata should be json: {err}")) +} diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 0bfd8ec876..dbcc12084a 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::test_path_buf_with_windows; use app_test_support::test_tmp_path_buf; use app_test_support::to_response; @@ -58,7 +58,7 @@ sandbox_mode = "workspace-write" let codex_home_path = codex_home.path().canonicalize()?; let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -108,7 +108,7 @@ allowed_domains = ["example.com"] let codex_home_path = codex_home.path().canonicalize()?; let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -179,7 +179,7 @@ forced_chatgpt_workspace_id = "{WORKSPACE_ID}" ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -218,7 +218,7 @@ forced_chatgpt_workspace_id = ["{WORKSPACE_ID_A}", "{WORKSPACE_ID_B}"] ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -260,7 +260,7 @@ location = { country = "US", city = "New York", timezone = "America/New_York" } "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -304,7 +304,7 @@ web_search = true "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -340,7 +340,7 @@ default_tools_approval_mode = "prompt" let codex_home_path = codex_home.path().canonicalize()?; let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -427,7 +427,7 @@ width = 320 "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -474,7 +474,7 @@ model_reasoning_effort = "high" set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; let project_config = AbsolutePathBuf::try_from(project_config_dir)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -544,7 +544,7 @@ writable_roots = [{}] let managed_path_str = managed_path.display().to_string(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[( "CODEX_APP_SERVER_MANAGED_CONFIG_PATH", @@ -640,7 +640,7 @@ model = "gpt-old" "#, )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -701,7 +701,7 @@ async fn config_value_write_updates_desktop_settings() -> Result<()> { let codex_home = temp_dir.path().canonicalize()?; write_config(&temp_dir, "")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let write_id = mcp @@ -750,7 +750,7 @@ model = "gpt-old" "#, )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let write_id = mcp @@ -798,7 +798,7 @@ model = "gpt-old" "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let write_id = mcp @@ -833,7 +833,7 @@ async fn config_batch_write_applies_multiple_edits() -> Result<()> { let codex_home = tmp_dir.path().canonicalize()?; write_config(&tmp_dir, "")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let writable_root = test_tmp_path_buf(); @@ -905,7 +905,7 @@ model = "gpt-5.3-spark" "#, )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let batch_id = mcp @@ -961,7 +961,7 @@ async fn config_batch_write_updates_multiple_desktop_settings() -> Result<()> { let codex_home = tmp_dir.path().canonicalize()?; write_config(&tmp_dir, "")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let batch_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs index cf76b9d573..0e00bc6309 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs @@ -758,12 +758,15 @@ pub(super) async fn read_response_and_notification_for_method( JSONRPCMessage::Response(candidate) if candidate.id == target_id => { response = Some(candidate); } + JSONRPCMessage::Notification(candidate) + if candidate.method == method && notification.is_some() => + { + bail!( + "received duplicate notification for method `{method}` before completing paired read" + ); + } JSONRPCMessage::Notification(candidate) if candidate.method == method => { - if notification.replace(candidate).is_some() { - bail!( - "received duplicate notification for method `{method}` before completing paired read" - ); - } + notification = Some(candidate); } _ => {} } diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs index 4b708d4ab3..0c8a59a9c8 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket_unix.rs @@ -226,6 +226,7 @@ async fn send_turn_start_request(stream: &mut WsClient, id: i64, thread_id: &str id, Some(serde_json::to_value(TurnStartParams { thread_id: thread_id.to_string(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index b357e139db..79c5b801bd 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; @@ -51,7 +51,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Use a minimal JSON schema so we can assert the tool payload round-trips. @@ -89,6 +89,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -134,7 +135,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let dynamic_tool = DynamicToolSpec { @@ -168,6 +169,7 @@ async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Resu let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -206,7 +208,7 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let dynamic_tool = DynamicToolSpec { @@ -246,7 +248,7 @@ async fn thread_start_rejects_dynamic_tools_not_supported_by_responses() -> Resu let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let dynamic_tool = DynamicToolSpec { @@ -311,7 +313,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let dynamic_tool = DynamicToolSpec { @@ -347,6 +349,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -486,7 +489,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let dynamic_tool = DynamicToolSpec { @@ -521,6 +524,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Run the tool".to_string(), text_elements: Vec::new(), @@ -704,7 +708,7 @@ fn function_call_output_raw_output(body: &Value, call_id: &str) -> Option } async fn wait_for_dynamic_tool_started( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, call_id: &str, ) -> Result { loop { @@ -724,7 +728,7 @@ async fn wait_for_dynamic_tool_started( } async fn wait_for_dynamic_tool_completed( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, call_id: &str, ) -> Result { loop { diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index 45b2e45e3e..fc1100d17b 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -1,6 +1,6 @@ use anyhow::Result; use app_test_support::DEFAULT_CLIENT_NAME; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use codex_app_server_protocol::AskForApproval; @@ -30,7 +30,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] async fn mock_experimental_method_requires_experimental_api_capability() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( @@ -61,7 +61,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu #[tokio::test] async fn realtime_conversation_start_requires_experimental_api_capability() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( @@ -99,7 +99,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R #[tokio::test] async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( @@ -133,7 +133,7 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result #[tokio::test] async fn thread_settings_update_requires_experimental_api_capability() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( @@ -167,7 +167,7 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result #[tokio::test] async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( @@ -210,7 +210,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( default_client_info(), @@ -248,7 +248,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( default_client_info(), @@ -285,7 +285,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let init = mcp .initialize_with_capabilities( default_client_info(), diff --git a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs index d6101995fa..60a0f94839 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; @@ -50,7 +50,7 @@ async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Resu )) .build() .await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -134,7 +134,7 @@ async fn experimental_feature_list_marks_apps_and_plugins_disabled_by_workspace_ .mount(&server) .await; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -195,7 +195,7 @@ memories = true "#, )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -229,7 +229,7 @@ memories = true #[tokio::test] async fn experimental_feature_list_rejects_unknown_thread_id() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -264,7 +264,7 @@ async fn experimental_feature_enablement_set_applies_to_global_and_thread_config let project_cwd = codex_home.path().join("project"); std::fs::create_dir_all(&project_cwd)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let actual = @@ -299,7 +299,7 @@ async fn experimental_feature_enablement_set_does_not_override_user_config() -> codex_home.path().join("config.toml"), "[features]\nmemories = false\n", )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let actual = set_experimental_feature_enablement( @@ -330,7 +330,7 @@ async fn experimental_feature_enablement_set_does_not_override_user_config() -> #[tokio::test] async fn experimental_feature_enablement_set_only_updates_named_features() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; set_experimental_feature_enablement(&mut mcp, BTreeMap::from([("apps".to_string(), true)])) @@ -340,6 +340,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re BTreeMap::from([ ("memories".to_string(), true), ("plugins".to_string(), true), + ("remote_plugin".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), ]), @@ -352,6 +353,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re enablement: BTreeMap::from([ ("memories".to_string(), true), ("plugins".to_string(), true), + ("remote_plugin".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), ]), @@ -381,6 +383,13 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re .and_then(|features| features.get("plugins")), Some(&json!(true)) ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("remote_plugin")), + Some(&json!(true)) + ); assert_eq!( config .additional @@ -402,7 +411,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re #[tokio::test] async fn experimental_feature_enablement_set_allows_remote_control() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let remote_control_enabled = false; let enablement = BTreeMap::from([("remote_control".to_string(), remote_control_enabled)]); @@ -420,7 +429,7 @@ async fn experimental_feature_enablement_set_allows_remote_control() -> Result<( #[tokio::test] async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; set_experimental_feature_enablement(&mut mcp, BTreeMap::from([("apps".to_string(), true)])) @@ -450,7 +459,7 @@ async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()> #[tokio::test] async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -474,7 +483,7 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() - ); assert!( error.message.contains( - "apps, memories, mentions_v2, plugins, remote_control, tool_suggest, tool_call_mcp_elicitation" + "apps, memories, mentions_v2, plugins, remote_control, remote_plugin, tool_suggest, tool_call_mcp_elicitation" ), "{}", error.message @@ -484,7 +493,7 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() - } async fn set_experimental_feature_enablement( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, enablement: BTreeMap, ) -> Result { let request_id = mcp @@ -495,7 +504,7 @@ async fn set_experimental_feature_enablement( read_response(mcp, request_id).await } -async fn read_config(mcp: &mut McpProcess, cwd: Option) -> Result { +async fn read_config(mcp: &mut TestAppServer, cwd: Option) -> Result { let request_id = mcp .send_config_read_request(ConfigReadParams { include_layers: false, @@ -505,7 +514,7 @@ async fn read_config(mcp: &mut McpProcess, cwd: Option) -> Result(mcp: &mut McpProcess, request_id: i64) -> Result { +async fn read_response(mcp: &mut TestAppServer, request_id: i64) -> Result { let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(request_id)), diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index f5f74c0231..fdb996c2f4 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -1,7 +1,7 @@ use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml; @@ -38,7 +38,8 @@ async fn external_agent_config_import_sends_completion_notification_for_sync_onl let codex_home = TempDir::new()?; let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -117,7 +118,8 @@ async fn external_agent_config_import_sends_completion_notification_for_local_pl let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -205,7 +207,8 @@ async fn external_agent_config_import_sends_completion_notification_after_pendin let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -283,7 +286,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -391,6 +395,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> { let request_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "follow up".to_string(), text_elements: Vec::new(), @@ -455,7 +460,8 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -540,7 +546,8 @@ async fn external_agent_config_import_skips_already_imported_session_versions() let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -636,7 +643,8 @@ async fn external_agent_config_import_returns_before_background_session_import_f let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -765,7 +773,8 @@ async fn external_agent_config_import_rejects_undetected_session_paths() -> Resu let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -884,7 +893,8 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_ let home_dir = codex_home.path().display().to_string(); let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]) + .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -964,6 +974,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_ let request_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "follow up".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/fs.rs b/codex-rs/app-server/tests/suite/v2/fs.rs index 3ca511a482..1f04c847d2 100644 --- a/codex-rs/app-server/tests/suite/v2/fs.rs +++ b/codex-rs/app-server/tests/suite/v2/fs.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use base64::Engine; use base64::engine::general_purpose::STANDARD; @@ -36,14 +36,14 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60); const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); const OPTIONAL_FS_CHANGE_TIMEOUT: Duration = Duration::from_secs(2); -async fn initialized_mcp(codex_home: &TempDir) -> Result { - let mut mcp = McpProcess::new(codex_home.path()).await?; +async fn initialized_mcp(codex_home: &TempDir) -> Result { + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; Ok(mcp) } async fn expect_error_message( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, request_id: i64, expected_message: &str, ) -> Result<()> { @@ -125,7 +125,7 @@ async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result< let codex_home = TempDir::new()?; let absolute_file = codex_home.path().join("absolute.txt"); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))], ) @@ -853,7 +853,7 @@ fn fs_changed_notification(notification: JSONRPCNotification) -> Result Result> { match timeout( OPTIONAL_FS_CHANGE_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/hooks_list.rs b/codex-rs/app-server/tests/suite/v2/hooks_list.rs index 1c8f76fe51..b7270f317e 100644 --- a/codex-rs/app-server/tests/suite/v2/hooks_list.rs +++ b/codex-rs/app-server/tests/suite/v2/hooks_list.rs @@ -1,7 +1,7 @@ use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; @@ -135,7 +135,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> { let cwd = TempDir::new()?; write_user_hook_config(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -211,7 +211,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> { }"#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -270,7 +270,7 @@ async fn hooks_list_shows_plugin_hook_load_warnings() -> Result<()> { let cwd = TempDir::new()?; write_plugin_hook_config(codex_home.path(), "{ not-json")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -326,7 +326,7 @@ timeout = 5 )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -408,7 +408,7 @@ async fn hooks_list_uses_root_repo_hooks_for_linked_worktrees() -> Result<()> { write_project_hook_config(&worktree_root.join(".codex"), "echo worktree hook")?; set_project_trust_level(codex_home.path(), &repo_root, TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let list_id = mcp @@ -478,7 +478,7 @@ async fn config_batch_write_toggles_user_hook() -> Result<()> { let cwd = TempDir::new()?; write_user_hook_config(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -629,7 +629,7 @@ command = "python3 {hook_script_path}" ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let hook_list_id = mcp @@ -662,6 +662,7 @@ command = "python3 {hook_script_path}" let first_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "first turn".to_string(), text_elements: Vec::new(), @@ -723,6 +724,7 @@ command = "python3 {hook_script_path}" let second_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), @@ -792,6 +794,7 @@ command = "python3 {hook_script_path}" let third_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "third turn".to_string(), text_elements: Vec::new(), @@ -876,7 +879,7 @@ command = "python3 {hook_script_path}" ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let hook_list_id = mcp @@ -932,6 +935,7 @@ command = "python3 {hook_script_path}" let first_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "first turn".to_string(), text_elements: Vec::new(), @@ -983,6 +987,7 @@ command = "python3 {hook_script_path}" let second_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 47cacdb20e..0cf8504d3b 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; @@ -33,7 +33,7 @@ async fn initialize_uses_client_info_name_as_originator() -> Result<()> { let codex_home = TempDir::new()?; let expected_codex_home = AbsolutePathBuf::try_from(codex_home.path().canonicalize()?)?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let message = timeout( DEFAULT_READ_TIMEOUT, @@ -68,7 +68,7 @@ async fn initialize_probe_does_not_override_originator() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let message = timeout( DEFAULT_READ_TIMEOUT, @@ -95,7 +95,7 @@ async fn initialize_codex_backend_does_not_override_originator() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let message = timeout( DEFAULT_READ_TIMEOUT, @@ -123,7 +123,7 @@ async fn initialize_respects_originator_override_env_var() -> Result<()> { let codex_home = TempDir::new()?; let expected_codex_home = AbsolutePathBuf::try_from(codex_home.path().canonicalize()?)?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[( "CODEX_INTERNAL_ORIGINATOR_OVERRIDE", @@ -165,7 +165,7 @@ async fn initialize_rejects_invalid_client_name() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", None)], ) @@ -200,7 +200,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu let server = create_mock_responses_server_sequence_unchecked(responses).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let message = timeout( DEFAULT_READ_TIMEOUT, @@ -282,7 +282,7 @@ async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<( ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout( DEFAULT_READ_TIMEOUT, mcp.initialize_with_client_info(ClientInfo { @@ -306,6 +306,7 @@ async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<( let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs index 5f470f617d..7dc9f745af 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_add.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_add.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MarketplaceAddParams; @@ -28,7 +28,7 @@ async fn marketplace_add_local_directory_source() -> Result<()> { r#"{"name":"sample"}"#, )?; std::fs::write(source.join("plugins/sample/marker.txt"), "local ref")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs b/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs index dc438499f3..c6808b8271 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_remove.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MarketplaceRemoveParams; @@ -53,7 +53,7 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> { write_installed_marketplace(codex_home.path(), "debug")?; let installed_root = marketplace_install_root(codex_home.path()).join("debug"); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -91,7 +91,7 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> { async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs index 8660497da5..5ed1f2c6f5 100644 --- a/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs @@ -4,7 +4,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MarketplaceUpgradeParams; @@ -127,7 +127,7 @@ fn expected_installed_root(codex_home: &Path, marketplace_name: &str) -> Result< } async fn send_marketplace_upgrade( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, marketplace_name: Option<&str>, ) -> Result { let request_id = mcp @@ -169,7 +169,7 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> { )?; disable_plugin_startup_tasks(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let debug_root = expected_installed_root(codex_home.path(), "debug")?; @@ -223,7 +223,7 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> { )?; disable_plugin_startup_tasks(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let tools_root = expected_installed_root(codex_home.path(), "tools")?; @@ -264,7 +264,7 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re )?; disable_plugin_startup_tasks(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?; assert!(first_response.errors.is_empty()); @@ -292,7 +292,7 @@ async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result< &configured_local_marketplace_update(&local_source.path().display().to_string()), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; for marketplace_name in ["missing", "local-only"] { diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index bddf57f666..37b7a10d9b 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use axum::Router; @@ -92,7 +92,7 @@ stream_max_retries = 0 AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -157,7 +157,7 @@ apps = true AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_request_id = mcp @@ -288,11 +288,8 @@ struct ResourceAppsMcpServer; impl ServerHandler for ResourceAppsMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - protocol_version: ProtocolVersion::V_2025_06_18, - capabilities: ServerCapabilities::builder().enable_resources().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_resources().build()) + .with_protocol_version(ProtocolVersion::V_2025_06_18) } async fn read_resource( @@ -308,21 +305,19 @@ impl ServerHandler for ResourceAppsMcpServer { )); } - Ok(ReadResourceResult { - contents: vec![ - ResourceContents::TextResourceContents { - uri: TEST_RESOURCE_URI.to_string(), - mime_type: Some("text/markdown".to_string()), - text: TEST_RESOURCE_TEXT.to_string(), - meta: None, - }, - ResourceContents::BlobResourceContents { - uri: TEST_BLOB_RESOURCE_URI.to_string(), - mime_type: Some("application/octet-stream".to_string()), - blob: TEST_RESOURCE_BLOB.to_string(), - meta: None, - }, - ], - }) + Ok(ReadResourceResult::new(vec![ + ResourceContents::TextResourceContents { + uri: TEST_RESOURCE_URI.to_string(), + mime_type: Some("text/markdown".to_string()), + text: TEST_RESOURCE_TEXT.to_string(), + meta: None, + }, + ResourceContents::BlobResourceContents { + uri: TEST_BLOB_RESOURCE_URI.to_string(), + mime_type: Some("application/octet-stream".to_string()), + blob: TEST_RESOURCE_BLOB.to_string(), + meta: None, + }, + ])) } } diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs index 13ebe0b99c..8d0387ee97 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use axum::Json; @@ -116,7 +116,7 @@ async fn mcp_server_elicitation_round_trip() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -135,6 +135,7 @@ async fn mcp_server_elicitation_round_trip() -> Result<()> { let warmup_turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Warm up connectors.".to_string(), text_elements: Vec::new(), @@ -167,6 +168,7 @@ async fn mcp_server_elicitation_round_trip() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Use [$calendar](app://calendar) to run the calendar tool.".to_string(), text_elements: Vec::new(), @@ -308,11 +310,8 @@ struct ElicitationAppsMcpServer; impl ServerHandler for ElicitationAppsMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - protocol_version: rmcp::model::ProtocolVersion::V_2025_06_18, - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + .with_protocol_version(rmcp::model::ProtocolVersion::V_2025_06_18) } async fn list_tools( diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs index bc9839b530..2c34684ada 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml; @@ -20,6 +20,7 @@ use codex_core::config::set_project_trust_level; use codex_protocol::config_types::TrustLevel; use pretty_assertions::assert_eq; use rmcp::handler::server::ServerHandler; +use rmcp::model::Implementation; use rmcp::model::JsonObject; use rmcp::model::ListResourceTemplatesResult; use rmcp::model::ListResourcesResult; @@ -66,7 +67,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -99,6 +100,13 @@ url = "{mcp_server_url}/mcp" .map(|tool| tool.name.as_str()), Some("look-up.raw") ); + assert_eq!( + status + .server_info + .as_ref() + .and_then(|info| info.title.as_deref()), + Some("Lookup Server") + ); mcp_server_handle.abort(); let _ = mcp_server_handle.await; @@ -124,7 +132,7 @@ async fn mcp_server_status_list_uses_thread_project_local_config() -> Result<()> std::fs::create_dir_all(workspace.path().join(".git"))?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -205,10 +213,9 @@ struct McpStatusServer { impl ServerHandler for McpStatusServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_server_info( + Implementation::new("lookup-server", "1.0.0").with_title("Lookup Server"), + ) } async fn list_tools( @@ -244,13 +251,12 @@ struct SlowInventoryServer { impl ServerHandler for SlowInventoryServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder() + ServerInfo::new( + ServerCapabilities::builder() .enable_tools() .enable_resources() .build(), - ..ServerInfo::default() - } + ) } async fn list_tools( @@ -330,7 +336,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -395,7 +401,7 @@ url = "{underscore_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index a21dd0d9f0..10c8a6ef04 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::to_response; @@ -94,7 +94,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -161,7 +161,7 @@ url = "{mcp_server_url}/mcp" #[tokio::test] async fn mcp_server_tool_call_returns_error_for_unknown_thread() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -212,7 +212,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -322,7 +322,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -442,7 +442,7 @@ url = "{mcp_server_url}/mcp" )); std::fs::write(config_path, config_toml)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -461,6 +461,7 @@ url = "{mcp_server_url}/mcp" let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Call the large MCP tool".to_string(), text_elements: Vec::new(), @@ -537,10 +538,7 @@ struct ToolAppsMcpServer; impl ServerHandler for ToolAppsMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) } async fn list_tools( @@ -685,7 +683,7 @@ async fn start_mcp_server() -> Result<(String, JoinHandle<()>)> { } async fn wait_for_mcp_tool_call_completed( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, call_id: &str, ) -> Result { loop { diff --git a/codex-rs/app-server/tests/suite/v2/memory_reset.rs b/codex-rs/app-server/tests/suite/v2/memory_reset.rs index 7b2eddeb53..07e16a3ba6 100644 --- a/codex-rs/app-server/tests/suite/v2/memory_reset.rs +++ b/codex-rs/app-server/tests/suite/v2/memory_reset.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use chrono::Utc; use codex_app_server_protocol::JSONRPCResponse; @@ -36,7 +36,7 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result let thread_id = seed_stage1_output(&state_db, codex_home.path()).await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 92e0f126f3..1605983705 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -68,4 +68,5 @@ mod turn_interrupt; mod turn_start; mod turn_start_zsh_fork; mod turn_steer; +mod web_search; mod windows_sandbox_setup; diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index 94dcca4377..778577f857 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use app_test_support::write_models_cache; @@ -95,7 +95,7 @@ fn expected_visible_models() -> Vec { async fn list_models_returns_all_models_with_large_limit() -> Result<()> { let codex_home = TempDir::new()?; write_models_cache(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -129,7 +129,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> { async fn list_models_includes_hidden_models() -> Result<()> { let codex_home = TempDir::new()?; write_models_cache(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -214,7 +214,8 @@ openai_base_url = "{server_uri}/v1" AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -256,7 +257,7 @@ openai_base_url = "{server_uri}/v1" async fn list_models_pagination_works() -> Result<()> { let codex_home = TempDir::new()?; write_models_cache(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; @@ -305,7 +306,7 @@ async fn list_models_pagination_works() -> Result<()> { async fn list_models_rejects_invalid_cursor() -> Result<()> { let codex_home = TempDir::new()?; write_models_cache(codex_home.path())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; diff --git a/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs b/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs index 6dcb9ac4ec..4143e825ea 100644 --- a/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs +++ b/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs @@ -1,7 +1,7 @@ use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ModelProviderCapabilitiesReadParams; @@ -16,7 +16,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); #[tokio::test] async fn read_default_provider_capabilities() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -46,7 +46,7 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> { r#"model_provider = "amazon-bedrock" "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -60,7 +60,7 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> { let received: ModelProviderCapabilitiesReadResponse = to_response(response)?; let expected = ModelProviderCapabilitiesReadResponse { - namespace_tools: false, + namespace_tools: true, image_generation: false, web_search: false, }; diff --git a/codex-rs/app-server/tests/suite/v2/output_schema.rs b/codex-rs/app-server/tests/suite/v2/output_schema.rs index 149e098b68..bcd95068b3 100644 --- a/codex-rs/app-server/tests/suite/v2/output_schema.rs +++ b/codex-rs/app-server/tests/suite/v2/output_schema.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; @@ -32,7 +32,7 @@ async fn turn_start_accepts_output_schema_v2() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -59,6 +59,7 @@ async fn turn_start_accepts_output_schema_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -114,7 +115,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -141,6 +142,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { let turn_req_1 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -183,6 +185,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { let turn_req_2 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello again".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs index 5939065280..63e92af578 100644 --- a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -1,7 +1,7 @@ use std::time::Duration; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::PermissionProfileListParams; @@ -41,7 +41,7 @@ description = "Inspect without writes." "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -108,7 +108,7 @@ description = "Project-scoped profile." )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_request_id = mcp @@ -181,7 +181,7 @@ description = "Project-scoped profile." )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -221,7 +221,7 @@ description = "Project-scoped profile." } async fn read_response( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, request_id: i64, ) -> Result { let response: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/plan_item.rs b/codex-rs/app-server/tests/suite/v2/plan_item.rs index 97e67fa090..b5464231da 100644 --- a/codex-rs/app-server/tests/suite/v2/plan_item.rs +++ b/codex-rs/app-server/tests/suite/v2/plan_item.rs @@ -1,7 +1,7 @@ use anyhow::Result; use anyhow::anyhow; use anyhow::bail; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use codex_app_server_protocol::ItemCompletedNotification; @@ -53,7 +53,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let turn = start_plan_mode_turn(&mut mcp).await?; @@ -111,7 +111,7 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let _turn = start_plan_mode_turn(&mut mcp).await?; @@ -127,7 +127,7 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()> Ok(()) } -async fn start_plan_mode_turn(mcp: &mut McpProcess) -> Result { +async fn start_plan_mode_turn(mcp: &mut TestAppServer) -> Result { let thread_req = mcp .send_thread_start_request(ThreadStartParams { model: Some("mock-model".to_string()), @@ -152,6 +152,7 @@ async fn start_plan_mode_turn(mcp: &mut McpProcess) -> Result Result Result<( Vec, Vec, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 9d17e8747f..2de63f1451 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -1,13 +1,15 @@ use std::borrow::Cow; use std::sync::Arc; use std::sync::Mutex as StdMutex; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::time::Duration; use anyhow::Result; use anyhow::bail; use app_test_support::ChatGptAuthFixture; use app_test_support::DEFAULT_CLIENT_NAME; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::start_analytics_events_server; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; @@ -21,6 +23,8 @@ use axum::http::header::AUTHORIZATION; use axum::routing::get; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppSummary; +use codex_app_server_protocol::AppsListParams; +use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginAvailability; @@ -68,7 +72,7 @@ const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str = #[tokio::test] async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -95,7 +99,7 @@ async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> { #[tokio::test] async fn plugin_install_rejects_missing_install_source() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -124,7 +128,7 @@ async fn plugin_install_rejects_missing_install_source() -> Result<()> { #[tokio::test] async fn plugin_install_rejects_multiple_install_sources() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -161,7 +165,7 @@ async fn plugin_install_rejects_remote_marketplace_when_plugins_are_disabled() - plugins = false "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -227,7 +231,7 @@ async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()> ) .await; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -298,7 +302,7 @@ async fn plugin_install_rejects_missing_remote_bundle_url() -> Result<()> { .await; mount_empty_remote_installed_plugins(&server).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?; @@ -339,7 +343,7 @@ async fn plugin_install_rejects_plain_http_remote_bundle_url() -> Result<()> { mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await; mount_empty_remote_installed_plugins(&server).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?; @@ -385,7 +389,7 @@ async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()> { .await; mount_empty_remote_installed_plugins(&server).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?; @@ -417,7 +421,7 @@ async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()> { async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> { let codex_home = TempDir::new()?; write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -460,7 +464,7 @@ async fn plugin_install_rejects_remote_plugin_disabled_by_admin_before_download( .await; mount_empty_remote_installed_plugins(&server).await; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -540,7 +544,7 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -569,7 +573,7 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul #[tokio::test] async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -610,7 +614,7 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -660,7 +664,7 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; let mut mcp = - McpProcess::new_with_args(codex_home.path(), &["--session-source", "atlas"]).await?; + TestAppServer::new_with_args(codex_home.path(), &["--session-source", "atlas"]).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -709,7 +713,7 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> { let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -764,7 +768,7 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> { mount_remote_plugin_install(&server, REMOTE_PLUGIN_ID).await; mount_backend_analytics_events(&server).await; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -816,7 +820,7 @@ async fn plugin_install_errors_when_remote_bundle_download_fails() -> Result<()> mount_empty_remote_installed_plugins(&server).await; mount_remote_plugin_install(&server, REMOTE_PLUGIN_ID).await; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -890,7 +894,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { }, ]; let tools = vec![connector_tool("beta", "Beta App")?]; - let (server_url, server_handle) = start_apps_server(connectors, tools).await?; + let (server_url, server_handle, server_control) = start_apps_server(connectors, tools).await?; let codex_home = TempDir::new()?; write_connectors_config(codex_home.path(), &server_url)?; @@ -916,8 +920,9 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let directory_requests_before_install = server_control.directory_request_count(); let request_id = mcp .send_plugin_install_request(PluginInstallParams { @@ -947,6 +952,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { }], } ); + assert!(server_control.directory_request_count() > directory_requests_before_install); server_handle.abort(); let _ = server_handle.await; @@ -970,7 +976,8 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { is_enabled: true, plugin_display_names: Vec::new(), }]; - let (server_url, server_handle) = start_apps_server(connectors, Vec::new()).await?; + let (server_url, server_handle, server_control) = + start_apps_server(connectors, Vec::new()).await?; let codex_home = TempDir::new()?; write_connectors_config(codex_home.path(), &server_url)?; @@ -1000,8 +1007,10 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let directory_requests_before_install = + warm_app_directory_cache(&mut mcp, &server_control, "Alpha").await?; let request_id = mcp .send_plugin_install_request(PluginInstallParams { @@ -1031,6 +1040,10 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { }], } ); + assert_eq!( + server_control.directory_request_count(), + directory_requests_before_install + ); server_handle.abort(); let _ = server_handle.await; @@ -1067,7 +1080,7 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1113,6 +1126,46 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests #[derive(Clone)] struct AppsServerState { response: Arc>, + directory_request_count: Arc, +} + +#[derive(Clone)] +struct AppsServerControl { + directory_request_count: Arc, +} + +impl AppsServerControl { + fn directory_request_count(&self) -> usize { + self.directory_request_count.load(Ordering::SeqCst) + } +} + +async fn warm_app_directory_cache( + mcp: &mut TestAppServer, + server_control: &AppsServerControl, + expected_app_name: &str, +) -> Result { + let app_list_request_id = mcp + .send_apps_list_request(AppsListParams { + force_refetch: true, + ..Default::default() + }) + .await?; + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(app_list_request_id)), + ) + .await??; + let response: AppsListResponse = to_response(response)?; + assert!( + response + .data + .iter() + .any(|app| app.name == expected_app_name) + ); + let directory_request_count = server_control.directory_request_count(); + assert!(directory_request_count > 0); + Ok(directory_request_count) } #[derive(Clone)] @@ -1122,10 +1175,7 @@ struct PluginInstallMcpServer { impl ServerHandler for PluginInstallMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) } fn list_tools( @@ -1152,12 +1202,17 @@ impl ServerHandler for PluginInstallMcpServer { async fn start_apps_server( connectors: Vec, tools: Vec, -) -> Result<(String, JoinHandle<()>)> { +) -> Result<(String, JoinHandle<()>, AppsServerControl)> { + let directory_request_count = Arc::new(AtomicUsize::new(0)); let state = Arc::new(AppsServerState { response: Arc::new(StdMutex::new( json!({ "apps": connectors, "next_token": null }), )), + directory_request_count: directory_request_count.clone(), }); + let server_control = AppsServerControl { + directory_request_count, + }; let tools = Arc::new(StdMutex::new(tools)); let listener = TcpListener::bind("127.0.0.1:0").await?; @@ -1187,7 +1242,7 @@ async fn start_apps_server( let _ = axum::serve(listener, router).await; }); - Ok((format!("http://{addr}"), handle)) + Ok((format!("http://{addr}"), handle, server_control)) } async fn list_directory_connectors( @@ -1195,6 +1250,8 @@ async fn list_directory_connectors( headers: HeaderMap, uri: Uri, ) -> Result { + state.directory_request_count.fetch_add(1, Ordering::SeqCst); + let bearer_ok = headers .get(AUTHORIZATION) .and_then(|value| value.to_str().ok()) @@ -1527,7 +1584,7 @@ async fn mount_remote_plugin_install_after_cache_write( } async fn send_remote_plugin_install_request( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, remote_plugin_id: &str, ) -> Result { mcp.send_plugin_install_request(PluginInstallParams { diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 93dbfb4773..ea9e5f4f35 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::bail; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::JSONRPCResponse; @@ -81,7 +81,7 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul std::fs::write(marketplace_path.as_path(), "{not json")?; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -146,7 +146,7 @@ enabled = true "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -180,6 +180,107 @@ enabled = true Ok(()) } +#[tokio::test] +async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled() -> Result<()> +{ + let codex_home = TempDir::new()?; + let server = MockServer::start().await; + write_openai_curated_marketplace(codex_home.path(), &["linear", "calendar"])?; + write_installed_plugin(&codex_home, "openai-curated", "linear")?; + write_installed_plugin(&codex_home, "openai-curated", "calendar")?; + std::fs::write( + codex_home.path().join("config.toml"), + format!( + r#"chatgpt_base_url = "{}/backend-api/" + +[features] +plugins = true +remote_plugin = true +plugin_sharing = false + +[plugins."linear@openai-curated"] +enabled = true + +[plugins."calendar@openai-curated"] +enabled = true +"#, + server.uri() + ), + )?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .chatgpt_user_id("user-123") + .chatgpt_account_id("account-123"), + AuthCredentialsStoreMode::File, + )?; + let mut global_installed_body: serde_json::Value = serde_json::from_str( + &remote_installed_plugin_body("", "1.2.3", /*enabled*/ true), + )?; + let mut remote_only = global_installed_body["plugins"][0].clone(); + remote_only["id"] = serde_json::json!("plugins~Plugin_11111111111111111111111111111111"); + remote_only["name"] = serde_json::json!("remote-only"); + remote_only["release"]["display_name"] = serde_json::json!("Remote Only"); + global_installed_body["plugins"] + .as_array_mut() + .expect("installed plugins should be an array") + .push(remote_only); + let global_installed_body = serde_json::to_string(&global_installed_body)?; + mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await; + mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) + .await; + + let mut app_server = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??; + + let request_id = app_server + .send_plugin_installed_request(PluginInstalledParams { + cwds: None, + install_suggestion_plugin_names: None, + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginInstalledResponse = to_response(response)?; + + let local_marketplace = response + .marketplaces + .iter() + .find(|marketplace| marketplace.name == "openai-curated") + .expect("expected openai-curated marketplace entry"); + assert_eq!( + local_marketplace + .plugins + .iter() + .map(|plugin| plugin.id.clone()) + .collect::>(), + vec!["calendar@openai-curated".to_string()] + ); + let remote_marketplace = response + .marketplaces + .iter() + .find(|marketplace| marketplace.name == "openai-curated-remote") + .expect("expected openai-curated-remote marketplace entry"); + assert_eq!( + remote_marketplace + .plugins + .iter() + .map(|plugin| plugin.id.clone()) + .collect::>(), + vec![ + "linear@openai-curated-remote".to_string(), + "remote-only@openai-curated-remote".to_string(), + ] + ); + assert_eq!(response.marketplace_load_errors, Vec::new()); + Ok(()) +} + #[tokio::test] async fn plugin_installed_ignores_local_cache_without_catalog() -> Result<()> { let codex_home = TempDir::new()?; @@ -194,7 +295,7 @@ enabled = true "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -219,7 +320,7 @@ enabled = true #[tokio::test] async fn plugin_list_rejects_relative_cwds() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -296,7 +397,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ std::fs::write(invalid_marketplace_path.as_path(), "{not json")?; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -413,7 +514,7 @@ async fn plugin_list_returns_empty_when_workspace_codex_plugins_disabled() -> Re .await; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_without_managed_config_with_env( + let mut mcp = TestAppServer::new_without_managed_config_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -504,7 +605,7 @@ async fn plugin_list_reuses_cached_workspace_codex_plugins_setting() -> Result<( .await; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_without_managed_config_with_env( + let mut mcp = TestAppServer::new_without_managed_config_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -589,7 +690,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab )?; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -702,7 +803,7 @@ async fn plugin_list_accepts_omitted_cwds() -> Result<()> { }"#, )?; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -762,7 +863,7 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<( &AbsolutePathBuf::try_from(plugin_root)?, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -854,7 +955,7 @@ enabled = false "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1001,7 +1102,7 @@ enabled = false let workspace_default = TempDir::new()?; let home = codex_home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home.as_str())), @@ -1095,7 +1196,7 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res }"##, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1208,7 +1309,7 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> { }"##, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1296,7 +1397,7 @@ enabled = true "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1399,7 +1500,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { .join(STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE); { - let mut mcp = McpProcess::new_with_plugin_startup_tasks(codex_home.path()).await?; + let mut mcp = TestAppServer::new_with_plugin_startup_tasks(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; wait_for_path_exists(&marker_path).await?; @@ -1438,7 +1539,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { assert!(config.contains(r#"[plugins."linear@openai-curated"]"#)); { - let mut mcp = McpProcess::new_with_plugin_startup_tasks(codex_home.path()).await?; + let mut mcp = TestAppServer::new_with_plugin_startup_tasks(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; } @@ -1490,7 +1591,7 @@ async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() -> let installed_path = codex_home .path() .join("plugins/cache/openai-curated-remote/linear/1.2.3"); - let mut mcp = McpProcess::new_with_env_and_plugin_startup_tasks( + let mut mcp = TestAppServer::new_with_env_and_plugin_startup_tasks( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -1568,7 +1669,7 @@ async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles() .path() .join("plugins/cache/openai-curated-remote/stale"); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -1738,7 +1839,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1862,7 +1963,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested() mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1949,7 +2050,7 @@ async fn plugin_list_fail_opens_openai_curated_remote_collection_errors() -> Res mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1991,7 +2092,7 @@ async fn plugin_list_does_not_query_openai_curated_remote_collection_by_default( AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2044,7 +2145,7 @@ async fn plugin_list_vertical_kind_noops_when_remote_plugin_enabled() -> Result< AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2098,7 +2199,7 @@ async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_ex AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2175,7 +2276,7 @@ plugin_sharing = true mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await; mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2264,7 +2365,7 @@ plugin_sharing = false mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body()) .await; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))], ) @@ -2338,7 +2439,7 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await; mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2455,7 +2556,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await; mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2612,7 +2713,7 @@ plugin_sharing = false AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2747,7 +2848,7 @@ async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> { .await; } - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2807,7 +2908,7 @@ remote_plugin = true AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2843,7 +2944,7 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -2882,7 +2983,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> .mount(&server) .await; - let mut mcp = McpProcess::new_with_plugin_startup_tasks(codex_home.path()).await?; + let mut mcp = TestAppServer::new_with_plugin_startup_tasks(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 2a5d3d887a..1f1c217ee5 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -5,7 +5,7 @@ use std::time::Duration; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use axum::Json; @@ -64,7 +64,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] async fn plugin_read_rejects_missing_read_source() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -93,7 +93,7 @@ async fn plugin_read_rejects_missing_read_source() -> Result<()> { #[tokio::test] async fn plugin_read_rejects_multiple_read_sources() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -190,7 +190,7 @@ plugins = true .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -297,7 +297,7 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result< .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; for remote_marketplace_name in [ @@ -478,7 +478,7 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() -> .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -563,7 +563,7 @@ async fn plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enable .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -615,7 +615,7 @@ async fn plugin_read_maps_missing_remote_plugin_to_invalid_request() -> Result<( .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -667,7 +667,7 @@ remote_plugin = true AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -697,7 +697,7 @@ remote_plugin = true async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()> { let codex_home = TempDir::new()?; write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -755,7 +755,7 @@ enabled = true )?; write_installed_plugin(&codex_home, "openai-curated", "demo-plugin")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let marketplace_path = @@ -855,7 +855,7 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<( .expect(1) .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -981,7 +981,7 @@ async fn plugin_read_keeps_remote_version_when_share_principals_are_missing() -> .expect(1) .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1037,7 +1037,7 @@ async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() -> let plugin_path = AbsolutePathBuf::try_from(repo_root.path().join("demo-plugin"))?; write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &plugin_path)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1095,7 +1095,7 @@ async fn plugin_read_fails_on_malformed_share_mapping() -> Result<()> { "not valid json\n", )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1287,7 +1287,7 @@ enabled = false )?; write_installed_plugin(&codex_home, "codex-curated", "demo-plugin")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let marketplace_path = @@ -1458,7 +1458,7 @@ async fn plugin_read_returns_app_needs_auth() -> Result<()> { let marketplace_path = AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1525,7 +1525,7 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> { )?; write_plugins_enabled_config(&codex_home)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1587,7 +1587,7 @@ async fn plugin_read_describes_uninstalled_git_source_without_cloning() -> Resul )?; write_plugins_enabled_config(&codex_home)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1650,7 +1650,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result< )?; write_plugins_enabled_config(&codex_home)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1703,7 +1703,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() - )?; write_plugins_enabled_config(&codex_home)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1768,10 +1768,7 @@ struct PluginReadMcpServer { impl ServerHandler for PluginReadMcpServer { fn get_info(&self) -> ServerInfo { - ServerInfo { - capabilities: ServerCapabilities::builder().enable_tools().build(), - ..ServerInfo::default() - } + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) } fn list_tools( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs index 69f93f0b7e..99fc1289d4 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -4,7 +4,7 @@ use std::time::Duration; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::JSONRPCError; @@ -102,7 +102,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let expected_plugin_path = AbsolutePathBuf::try_from(plugin_path.clone())?; let request_id = mcp @@ -251,7 +251,7 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let expected_plugin_path = AbsolutePathBuf::try_from(plugin_path)?; let request_id = mcp @@ -304,7 +304,7 @@ async fn plugin_share_save_rejects_listed_discoverability() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -359,7 +359,7 @@ plugin_sharing = false AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -404,7 +404,7 @@ async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -482,7 +482,7 @@ async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -555,7 +555,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request("plugin/share/list", Some(json!({}))) @@ -626,7 +626,7 @@ async fn plugin_share_checkout_adds_personal_marketplace_entry() -> Result<()> { mount_empty_remote_installed_plugins(&server, "WORKSPACE").await; let home_env = home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home_env.as_str())), @@ -789,7 +789,7 @@ async fn plugin_share_checkout_rejects_non_share_remote_plugin() -> Result<()> { mount_empty_remote_installed_plugins(&server, "GLOBAL").await; let home_env = home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home_env.as_str())), @@ -880,7 +880,7 @@ async fn plugin_share_checkout_cleans_up_path_when_marketplace_update_fails() -> mount_empty_remote_installed_plugins(&server, "WORKSPACE").await; let home_env = home.path().to_string_lossy().into_owned(); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("HOME", Some(home_env.as_str())), @@ -983,7 +983,7 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -1065,7 +1065,7 @@ plugin_sharing = false AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( @@ -1114,7 +1114,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_raw_request( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs index 4feede934d..46299d1186 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs @@ -4,7 +4,7 @@ use anyhow::Result; use anyhow::bail; use app_test_support::ChatGptAuthFixture; use app_test_support::DEFAULT_CLIENT_NAME; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::start_analytics_events_server; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; @@ -42,7 +42,7 @@ enabled = true "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let params = PluginUninstallParams { @@ -100,7 +100,7 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> { AuthCredentialsStoreMode::File, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -161,7 +161,7 @@ async fn plugin_uninstall_rejects_remote_plugin_when_plugins_are_disabled() -> R plugins = false "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -230,7 +230,7 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl )); std::fs::create_dir_all(legacy_remote_plugin_cache_root.join("local/.codex-plugin"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -302,7 +302,7 @@ async fn plugin_uninstall_uses_detail_scope_for_cache_namespace() -> Result<()> .join("plugins/cache/openai-curated-remote/linear"); std::fs::create_dir_all(global_cache_root.join("1.0.0/.codex-plugin"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -376,7 +376,7 @@ async fn plugin_uninstall_accepts_workspace_remote_plugin_id_shape() -> Result<( r#"{"name":"skill-improver","version":"1.0.0"}"#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -425,7 +425,7 @@ async fn plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails() - )); std::fs::create_dir_all(legacy_remote_plugin_cache_root.join("local/.codex-plugin"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -467,7 +467,7 @@ async fn plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_ca codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -502,7 +502,7 @@ async fn plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call() codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -537,7 +537,7 @@ async fn plugin_uninstall_rejects_empty_remote_plugin_id() -> Result<()> { codex_home.path(), &format!("{}/backend-api/", server.uri()), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/process_exec.rs b/codex-rs/app-server/tests/suite/v2/process_exec.rs index d273257e6e..ac4f6eeda3 100644 --- a/codex-rs/app-server/tests/suite/v2/process_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/process_exec.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use codex_app_server_protocol::ProcessExitedNotification; use codex_app_server_protocol::ProcessKillParams; @@ -108,7 +108,7 @@ async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Res let codex_home = TempDir::new()?; let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))], ) @@ -230,10 +230,10 @@ async fn process_kill_terminates_running_process() -> Result<()> { Ok(()) } -async fn initialized_mcp(codex_home: &Path) -> Result<(MockServer, McpProcess)> { +async fn initialized_mcp(codex_home: &Path) -> Result<(MockServer, TestAppServer)> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; create_config_toml(codex_home, &server.uri(), "never")?; - let mut mcp = McpProcess::new(codex_home).await?; + let mut mcp = TestAppServer::new(codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; Ok((server, mcp)) } @@ -257,7 +257,7 @@ fn process_spawn_params( }) } -async fn read_process_exited(mcp: &mut McpProcess) -> Result { +async fn read_process_exited(mcp: &mut TestAppServer) -> Result { let notification = mcp .read_stream_until_notification_message("process/exited") .await?; diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index b15960c1e9..0eca044991 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -1,6 +1,6 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::AddCreditsNudgeCreditType; @@ -37,7 +37,8 @@ const INTERNAL_ERROR_CODE: i64 = -32603; async fn get_account_rate_limits_requires_auth() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_get_account_rate_limits_request().await?; @@ -62,7 +63,7 @@ async fn get_account_rate_limits_requires_auth() -> Result<()> { async fn get_account_rate_limits_requires_chatgpt_auth() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -153,7 +154,8 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_get_account_rate_limits_request().await?; @@ -238,7 +240,8 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { async fn send_add_credits_nudge_email_requires_auth() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -267,7 +270,7 @@ async fn send_add_credits_nudge_email_requires_auth() -> Result<()> { async fn send_add_credits_nudge_email_requires_chatgpt_auth() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -321,7 +324,8 @@ async fn send_add_credits_nudge_email_posts_expected_body() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -364,7 +368,8 @@ async fn send_add_credits_nudge_email_maps_cooldown() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -407,7 +412,8 @@ async fn send_add_credits_nudge_email_surfaces_backend_failure() -> Result<()> { .mount(&server) .await; - let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -437,7 +443,7 @@ async fn send_add_credits_nudge_email_surfaces_backend_failure() -> Result<()> { Ok(()) } -async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> { +async fn login_with_api_key(mcp: &mut TestAppServer, api_key: &str) -> Result<()> { let request_id = mcp.send_login_account_api_key_request(api_key).await?; let response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index 975819dc7f..f88fb54929 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; @@ -191,7 +191,7 @@ struct RealtimeSidebandScript { } struct RealtimeE2eHarness { - mcp: McpProcess, + mcp: TestAppServer, _codex_home: TempDir, main_loop_responses_server: MockServer, realtime_server: WebSocketTestServer, @@ -280,7 +280,7 @@ impl RealtimeE2eHarness { sandbox, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -539,7 +539,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { StartupContextConfig::Generated, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -788,7 +788,7 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript StartupContextConfig::Generated, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -890,7 +890,7 @@ async fn realtime_list_voices_returns_supported_names() -> Result<()> { StartupContextConfig::Generated, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -962,7 +962,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> { StartupContextConfig::Generated, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -1058,7 +1058,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> { StartupContextConfig::Override("startup context"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -1973,7 +1973,7 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> { StartupContextConfig::Override("startup context"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; login_with_api_key(&mut mcp, "sk-test-key").await?; @@ -2034,7 +2034,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> { StartupContextConfig::Generated, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread_start_request_id = mcp @@ -2074,7 +2074,10 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> { Ok(()) } -async fn read_notification(mcp: &mut McpProcess, method: &str) -> Result { +async fn read_notification( + mcp: &mut TestAppServer, + method: &str, +) -> Result { let notification = timeout( DEFAULT_TIMEOUT, mcp.read_stream_until_notification_message(method), @@ -2086,7 +2089,7 @@ async fn read_notification(mcp: &mut McpProcess, method: &s Ok(serde_json::from_value(params)?) } -async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> { +async fn login_with_api_key(mcp: &mut TestAppServer, api_key: &str) -> Result<()> { let request_id = mcp.send_login_account_api_key_request(api_key).await?; let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, @@ -2100,7 +2103,7 @@ async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> { } async fn wait_for_started_command_execution( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { loop { let started = read_notification::(mcp, "item/started").await?; @@ -2111,7 +2114,7 @@ async fn wait_for_started_command_execution( } async fn wait_for_completed_command_execution( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { loop { let completed = diff --git a/codex-rs/app-server/tests/suite/v2/remote_control.rs b/codex-rs/app-server/tests/suite/v2/remote_control.rs index d71f05bf58..d15f2d4187 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_control.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_control.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url; @@ -29,7 +29,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] async fn remote_control_disable_returns_disabled_status() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_remote_control_disable_request().await?; @@ -50,7 +50,7 @@ async fn remote_control_disable_returns_disabled_status() -> Result<()> { #[tokio::test] async fn remote_control_status_read_returns_disabled_status() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_remote_control_status_read_request().await?; @@ -72,7 +72,7 @@ async fn remote_control_status_read_returns_disabled_status() -> Result<()> { async fn remote_control_enable_returns_connecting_status() -> Result<()> { let codex_home = TempDir::new()?; let _backend = BlockingRemoteControlBackend::start(codex_home.path()).await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_remote_control_enable_request().await?; @@ -94,7 +94,7 @@ async fn remote_control_enable_returns_connecting_status() -> Result<()> { async fn remote_control_status_read_returns_connecting_status_after_enable() -> Result<()> { let codex_home = TempDir::new()?; let mut backend = BlockingRemoteControlBackend::start(codex_home.path()).await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp.send_remote_control_enable_request().await?; diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 9787868f5c..158ebb25d1 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -113,6 +113,7 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste request_id: RequestId::Integer(2), params: TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -228,7 +229,6 @@ fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()> { BTreeSet::from([ "config.toml".to_string(), "installation_id".to_string(), - "memories".to_string(), "skills".to_string(), ]), "non-local thread persistence should not create unexpected files in codex_home" diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index 7acdc1b12f..bea78901f5 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_request_permissions_sse_response; @@ -30,7 +30,7 @@ async fn request_permissions_round_trip() -> Result<()> { let server = create_mock_responses_server_sequence(responses).await; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -49,6 +49,7 @@ async fn request_permissions_round_trip() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "pick a directory".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/request_user_input.rs b/codex-rs/app-server/tests/suite/v2/request_user_input.rs index f77ddfb4f7..da61d34112 100644 --- a/codex-rs/app-server/tests/suite/v2/request_user_input.rs +++ b/codex-rs/app-server/tests/suite/v2/request_user_input.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_request_user_input_sse_response; @@ -32,7 +32,7 @@ async fn request_user_input_round_trip() -> Result<()> { let server = create_mock_responses_server_sequence(responses).await; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -51,6 +51,7 @@ async fn request_user_input_round_trip() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "ask something".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs index bf0271f821..427a9f288b 100644 --- a/codex-rs/app-server/tests/suite/v2/review.rs +++ b/codex-rs/app-server/tests/suite/v2/review.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::create_mock_responses_server_sequence; @@ -59,7 +59,7 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<() let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -91,6 +91,7 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<() turn.items, vec![ThreadItem::UserMessage { id: turn_id.clone(), + client_id: None, content: vec![V2UserInput::Text { text: "commit 1234567: Tidy UI colors".to_string(), text_elements: Vec::new(), @@ -172,7 +173,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() -> let codex_home = TempDir::new()?; create_config_toml_with_approval_policy(codex_home.path(), &server.uri(), "untrusted")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -199,6 +200,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() -> turn.items, vec![ThreadItem::UserMessage { id: turn_id.clone(), + client_id: None, content: vec![V2UserInput::Text { text: "commit 1234567: Check review approvals".to_string(), text_elements: Vec::new(), @@ -254,7 +256,7 @@ async fn review_start_rejects_empty_base_branch() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -297,7 +299,7 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<( let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -328,6 +330,7 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<( turn.items, vec![ThreadItem::UserMessage { id: turn.id.clone(), + client_id: None, content: vec![V2UserInput::Text { text: "detached review".to_string(), text_elements: Vec::new(), @@ -374,7 +377,7 @@ async fn review_start_rejects_empty_commit_sha() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -409,7 +412,7 @@ async fn review_start_rejects_empty_custom_instructions() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_default_thread(&mut mcp).await?; @@ -440,7 +443,7 @@ async fn review_start_rejects_empty_custom_instructions() -> Result<()> { Ok(()) } -async fn start_default_thread(mcp: &mut McpProcess) -> Result { +async fn start_default_thread(mcp: &mut TestAppServer) -> Result { let thread_req = mcp .send_thread_start_request(ThreadStartParams { model: Some("mock-model".to_string()), @@ -461,10 +464,11 @@ async fn start_default_thread(mcp: &mut McpProcess) -> Result { Ok(thread.id) } -async fn materialize_thread_rollout(mcp: &mut McpProcess, thread_id: &str) -> Result<()> { +async fn materialize_thread_rollout(mcp: &mut TestAppServer, thread_id: &str) -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.to_string(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "materialize rollout".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs index 7c1619e38a..c5b70b2334 100644 --- a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs +++ b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::CodexErrorInfo; use codex_app_server_protocol::ErrorNotification; @@ -48,7 +48,7 @@ async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -67,6 +67,7 @@ async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "trigger safeguard".to_string(), text_elements: Vec::new(), @@ -114,7 +115,7 @@ async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -133,6 +134,7 @@ async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "trigger cyber policy error".to_string(), text_elements: Vec::new(), @@ -190,7 +192,7 @@ async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -209,6 +211,7 @@ async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "trigger response model check".to_string(), text_elements: Vec::new(), @@ -258,7 +261,7 @@ async fn model_verification_emits_typed_notification_and_warning_v2() -> Result< let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -277,6 +280,7 @@ async fn model_verification_emits_typed_notification_and_warning_v2() -> Result< let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "trigger model verification".to_string(), text_elements: Vec::new(), @@ -306,7 +310,7 @@ async fn model_verification_emits_typed_notification_and_warning_v2() -> Result< } async fn collect_turn_notifications_and_validate_no_warning_item( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let mut rerouted = None; @@ -348,7 +352,7 @@ async fn collect_turn_notifications_and_validate_no_warning_item( } async fn collect_model_verification_notifications_and_validate_no_warning_item( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let mut verification = None; @@ -399,7 +403,7 @@ async fn collect_model_verification_notifications_and_validate_no_warning_item( } async fn collect_cyber_policy_error_and_validate_no_reroute( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let mut error = None; diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 74100bc19f..cba2f4ee3f 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; @@ -13,11 +13,14 @@ use codex_app_server_protocol::PluginListParams; use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SkillsChangedNotification; +use codex_app_server_protocol::SkillsExtraRootsSetParams; +use codex_app_server_protocol::SkillsExtraRootsSetResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::ThreadStartParams; use codex_config::types::AuthCredentialsStoreMode; use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -40,6 +43,23 @@ fn write_skill(root: &TempDir, name: &str) -> Result<()> { Ok(()) } +async fn expect_skills_changed_notification( + mcp: &mut TestAppServer, + timeout_duration: Duration, +) -> Result<()> { + let notification = timeout( + timeout_duration, + mcp.read_stream_until_notification_message("skills/changed"), + ) + .await??; + let params = notification + .params + .context("skills/changed params must be present")?; + let notification: SkillsChangedNotification = serde_json::from_value(params)?; + assert_eq!(notification, SkillsChangedNotification {}); + Ok(()) +} + fn write_plugins_enabled_config_with_base_url( codex_home: &std::path::Path, base_url: &str, @@ -222,7 +242,7 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result .mount(&server) .await; } - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let stale_skills_list_request_id = mcp @@ -346,7 +366,7 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable .mount(&server) .await; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -392,7 +412,7 @@ async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> { "---\nname: repo-skill\ndescription: from repo root\n---\n\n# Body\n", )?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))], ) @@ -436,7 +456,7 @@ async fn skills_list_accepts_relative_cwds() -> Result<()> { let relative_cwd = std::path::PathBuf::from("relative-cwd"); std::fs::create_dir_all(codex_home.path().join(&relative_cwd))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -464,7 +484,7 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> { let first_cwd = TempDir::new()?; let second_cwd = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -500,7 +520,7 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; // Seed the cwd cache before the cwd-local skill exists. @@ -573,6 +593,150 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> { Ok(()) } +#[tokio::test] +async fn skills_extra_roots_set_updates_process_runtime_roots() -> Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let extra_root = TempDir::new()?; + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + std::fs::create_dir_all(&skill_dir)?; + std::fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let set_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: vec![AbsolutePathBuf::from_absolute_path(&extra_skills_root)?], + }) + .await?; + let set_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(set_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(set_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + let missing_root = extra_root.path().join("missing-skills"); + let reset_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: vec![AbsolutePathBuf::from_absolute_path(&missing_root)?], + }) + .await?; + let reset_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(reset_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(reset_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let clear_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: Vec::new(), + }) + .await?; + let clear_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(clear_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(clear_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + drop(mcp); + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + Ok(()) +} + #[tokio::test] async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -585,7 +749,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( write_skill(&codex_home, "demo")?; let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, None)]) + TestAppServer::new_with_env(codex_home.path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, None)]) .await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let initial_skills_request_id = mcp @@ -631,7 +795,6 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( environments: None, mock_experimental_field: None, experimental_raw_events: false, - persist_extended_history: false, }) .await?; let _: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/thread_archive.rs b/codex-rs/app-server/tests/suite/v2/thread_archive.rs index b441a23cb6..7bf6fc9bc5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_archive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_archive.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_fake_rollout; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; @@ -38,7 +38,7 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a thread. @@ -93,6 +93,7 @@ async fn thread_archive_requires_materialized_rollout() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize".to_string(), text_elements: Vec::new(), @@ -220,7 +221,7 @@ async fn thread_archive_archives_spawned_descendants() -> Result<()> { ) .await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let archive_id = mcp @@ -341,7 +342,7 @@ async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()> { .join(child_rollout_path.file_name().expect("rollout file name")); std::fs::create_dir_all(&archived_child_path)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let archive_id = mcp @@ -445,7 +446,7 @@ async fn thread_archive_succeeds_when_spawned_descendant_is_missing() -> Result< ) .await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let archive_id = mcp @@ -498,7 +499,7 @@ async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -517,6 +518,7 @@ async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> let turn_start_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize".to_string(), text_elements: Vec::new(), @@ -537,7 +539,7 @@ async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> .await??; primary.clear_message_buffer(); - let mut secondary = McpProcess::new(codex_home.path()).await?; + let mut secondary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??; let archive_id = primary @@ -594,6 +596,7 @@ async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()> let resumed_turn_id = secondary .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![UserInput::Text { text: "secondary turn".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index cc89dd71cb..f89c9ec5e2 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -1,6 +1,6 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_fake_rollout; use app_test_support::create_fake_rollout_with_token_usage; use app_test_support::create_mock_responses_server_repeating_assistant; @@ -17,6 +17,7 @@ use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadListResponse; +use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -83,7 +84,7 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { ); let original_contents = std::fs::read_to_string(&original_path)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -241,7 +242,7 @@ async fn thread_fork_can_load_source_by_path() -> Result<()> { "rollout-2025-01-05T12-00-00-{conversation_id}.jsonl" )); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -281,7 +282,7 @@ async fn thread_fork_emits_restored_token_usage_before_next_turn() -> Result<()> Some("mock_provider"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -335,7 +336,7 @@ async fn thread_fork_can_exclude_turns_and_skip_restored_token_usage() -> Result Some("mock_provider"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -386,7 +387,7 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -405,7 +406,14 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "mock-model", "forked", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "mock-model", + "forked", + "user", + ); Ok(()) } @@ -415,7 +423,7 @@ async fn thread_fork_rejects_unmaterialized_thread() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -469,7 +477,7 @@ async fn thread_fork_with_empty_path_uses_thread_id() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -543,7 +551,7 @@ async fn thread_fork_surfaces_cloud_requirements_load_errors() -> Result<()> { )?; let refresh_token_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -606,7 +614,7 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<() /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -740,6 +748,7 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<() let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: fork_thread_id, + client_user_message_id: None, input: vec![UserInput::Text { text: "continue".to_string(), text_elements: Vec::new(), @@ -762,6 +771,120 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<() Ok(()) } +#[tokio::test] +async fn pathless_ephemeral_thread_rejects_codex_home_path_after_reload() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let parent_thread_id = create_fake_rollout( + codex_home.path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Parent message", + Some("mock_provider"), + /*git_info*/ None, + )?; + + let side_thread_id = { + let mut app_server = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??; + + let fork_id = app_server + .send_thread_fork_request(ThreadForkParams { + thread_id: parent_thread_id, + ephemeral: true, + ..Default::default() + }) + .await?; + let fork_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(fork_id)), + ) + .await??; + let ThreadForkResponse { thread, .. } = to_response::(fork_resp)?; + assert!(thread.ephemeral); + assert_eq!(thread.path, None); + + let turn_id = app_server + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: vec![UserInput::Text { + text: "continue".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + let _: TurnStartResponse = to_response::(turn_resp)?; + timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + thread.id + }; + + let mut app_server = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??; + let codex_home_path = codex_home.path().to_path_buf(); + + let resume_id = app_server + .send_thread_resume_request(ThreadResumeParams { + thread_id: side_thread_id.clone(), + path: Some(codex_home_path.clone()), + ..Default::default() + }) + .await?; + let resume_err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_error_message(RequestId::Integer(resume_id)), + ) + .await??; + assert!( + resume_err.error.message.contains("path is a directory"), + "unexpected resume error: {}", + resume_err.error.message + ); + assert!( + !resume_err.error.message.contains("Is a directory"), + "resume should reject the directory before rollout reading: {}", + resume_err.error.message + ); + + let fork_id = app_server + .send_thread_fork_request(ThreadForkParams { + thread_id: side_thread_id, + path: Some(codex_home_path), + ..Default::default() + }) + .await?; + let fork_err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + app_server.read_stream_until_error_message(RequestId::Integer(fork_id)), + ) + .await??; + assert!( + fork_err.error.message.contains("path is a directory"), + "unexpected fork error: {}", + fork_err.error.message + ); + assert!( + !fork_err.error.message.contains("Is a directory"), + "fork should reject the directory before rollout reading: {}", + fork_err.error.message + ); + + Ok(()) +} + // Helper to create a config.toml pointing at the mock model server. fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 5a45e81e1d..809358cc84 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; @@ -36,7 +36,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -92,6 +92,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -151,7 +152,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -170,6 +171,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() let first_turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "First turn".to_string(), text_elements: Vec::new(), @@ -215,6 +217,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() let second_turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Second turn".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_list.rs b/codex-rs/app-server/tests/suite/v2/thread_list.rs index e064ff6e25..0b49647128 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_list.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; +use app_test_support::create_fake_parented_rollout_with_source; use app_test_support::create_fake_rollout; use app_test_support::create_fake_rollout_with_source; use app_test_support::create_final_assistant_message_sse_response; @@ -47,14 +48,14 @@ use uuid::Uuid; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -async fn init_mcp(codex_home: &Path) -> Result { - let mut mcp = McpProcess::new(codex_home).await?; +async fn init_mcp(codex_home: &Path) -> Result { + let mut mcp = TestAppServer::new(codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; Ok(mcp) } async fn list_threads( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, cursor: Option, limit: Option, providers: Option>, @@ -74,7 +75,7 @@ async fn list_threads( } async fn list_threads_with_sort( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, cursor: Option, limit: Option, providers: Option>, @@ -227,6 +228,7 @@ async fn thread_list_reports_system_error_idle_flag_after_failed_turn() -> Resul let seed_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -249,6 +251,7 @@ async fn thread_list_reports_system_error_idle_flag_after_failed_turn() -> Resul let failed_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "fail turn".to_string(), text_elements: Vec::new(), @@ -1047,7 +1050,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> { let parent_thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?; - let review_id = create_fake_rollout_with_source( + let review_id = create_fake_parented_rollout_with_source( codex_home.path(), "2025-02-02T09-00-00", "2025-02-02T09:00:00Z", @@ -1055,6 +1058,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> { Some("mock_provider"), /*git_info*/ None, CoreSessionSource::SubAgent(SubAgentSource::Review), + parent_thread_id, )?; let compact_id = create_fake_rollout_with_source( codex_home.path(), @@ -1107,6 +1111,10 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> { .map(|thread| thread.id.as_str()) .collect(); assert_eq!(review_ids, vec![review_id.as_str()]); + assert_eq!( + review.data[0].parent_thread_id, + Some(parent_thread_id.to_string()) + ); let compact = list_threads( &mut mcp, diff --git a/codex-rs/app-server/tests/suite/v2/thread_loaded_list.rs b/codex-rs/app-server/tests/suite/v2/thread_loaded_list.rs index 245cfafb49..ef0c5d986b 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_loaded_list.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_loaded_list.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCResponse; @@ -21,7 +21,7 @@ async fn thread_loaded_list_returns_loaded_thread_ids() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -51,7 +51,7 @@ async fn thread_loaded_list_paginates() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let first = start_thread(&mut mcp).await?; @@ -122,7 +122,7 @@ stream_max_retries = 0 ) } -async fn start_thread(mcp: &mut McpProcess) -> Result { +async fn start_thread(mcp: &mut TestAppServer) -> Result { let req_id = mcp .send_thread_start_request(ThreadStartParams { model: Some("gpt-5.2".to_string()), diff --git a/codex-rs/app-server/tests/suite/v2/thread_memory_mode_set.rs b/codex-rs/app-server/tests/suite/v2/thread_memory_mode_set.rs index bf9bba7b2f..c93e966cc3 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_memory_mode_set.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_memory_mode_set.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_fake_rollout; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; @@ -27,7 +27,7 @@ async fn thread_memory_mode_set_updates_loaded_thread_state() -> Result<()> { create_config_toml(codex_home.path(), &server.uri())?; let state_db = init_state_db(codex_home.path()).await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -79,7 +79,7 @@ async fn thread_memory_mode_set_updates_stored_thread_state() -> Result<()> { )?; let thread_uuid = ThreadId::from_string(&thread_id)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; for mode in [ThreadMemoryMode::Disabled, ThreadMemoryMode::Enabled] { diff --git a/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs b/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs index c78e9b8152..fb61973466 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_metadata_update.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_fake_rollout; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::rollout_path; @@ -41,7 +41,7 @@ async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread() let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -136,7 +136,7 @@ async fn thread_metadata_update_rejects_empty_git_info_patch() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -182,7 +182,7 @@ async fn thread_metadata_update_rejects_ephemeral_thread() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -244,7 +244,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread() - /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let update_id = mcp @@ -309,7 +309,7 @@ async fn thread_metadata_update_repairs_loaded_thread_without_resetting_summary( ) .await; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -387,7 +387,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread() ); fs::rename(&archived_source, &archived_dest)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let update_id = mcp @@ -443,7 +443,7 @@ async fn thread_metadata_update_can_clear_stored_git_fields() -> Result<()> { )?; let _state_db = init_state_db(codex_home.path()).await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let update_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index c8e3e179fb..1540a84b58 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; 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; @@ -24,6 +24,7 @@ use codex_app_server_protocol::ThreadListResponse; use codex_app_server_protocol::ThreadNameUpdatedNotification; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadResumeInitialTurnsPageParams; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadSetNameParams; @@ -58,7 +59,6 @@ use codex_protocol::user_input::TextElement; use codex_thread_store::AppendThreadItemsParams; use codex_thread_store::CreateThreadParams; use codex_thread_store::InMemoryThreadStore; -use codex_thread_store::ThreadEventPersistenceMode; use codex_thread_store::ThreadMetadataPatch; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; @@ -103,7 +103,7 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -158,7 +158,7 @@ async fn thread_read_can_include_turns() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -216,7 +216,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> { append_user_message(rollout_path.as_path(), "2025-01-05T12:01:00Z", "second")?; append_user_message(rollout_path.as_path(), "2025-01-05T12:02:00Z", "third")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -305,7 +305,7 @@ async fn thread_turns_list_supports_requested_items_view() -> Result<()> { append_agent_message(rollout_path.as_path(), "2025-01-05T12:01:00Z", "draft")?; append_agent_message(rollout_path.as_path(), "2025-01-05T12:02:00Z", "final")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let full = read_single_turn_items_view( @@ -607,7 +607,7 @@ async fn thread_read_can_return_archived_threads_by_id() -> Result<()> { archived_dir.join(active_rollout_path.file_name().expect("rollout file name")); std::fs::rename(&active_rollout_path, &archived_rollout_path)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -631,6 +631,77 @@ async fn thread_read_can_return_archived_threads_by_id() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_initial_turns_page_matches_requested_turns_list_page() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let filename_ts = "2025-01-05T12-00-00"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home.path(), + filename_ts, + "2025-01-05T12:00:00Z", + "first", + vec![], + Some("mock_provider"), + /*git_info*/ None, + )?; + let rollout_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); + append_user_message(rollout_path.as_path(), "2025-01-05T12:01:00Z", "second")?; + append_user_message(rollout_path.as_path(), "2025-01-05T12:02:00Z", "third")?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let turns_list_id = mcp + .send_thread_turns_list_request(ThreadTurnsListParams { + thread_id: conversation_id.clone(), + cursor: None, + limit: Some(2), + sort_direction: Some(SortDirection::Asc), + items_view: Some(TurnItemsView::NotLoaded), + }) + .await?; + let turns_list_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turns_list_id)), + ) + .await??; + let expected_page = to_response::(turns_list_resp)?; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id, + exclude_turns: true, + initial_turns_page: Some(ThreadResumeInitialTurnsPageParams { + limit: Some(2), + sort_direction: Some(SortDirection::Asc), + items_view: Some(TurnItemsView::NotLoaded), + }), + ..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, + initial_turns_page, + .. + } = to_response::(resume_resp)?; + + assert!(thread.turns.is_empty()); + assert_eq!( + initial_turns_page, + Some(codex_app_server_protocol::TurnsPage::from(expected_page)) + ); + + Ok(()) +} + #[tokio::test] async fn thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -651,7 +722,7 @@ async fn thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back() -> R append_user_message(rollout_path.as_path(), "2025-01-05T12:01:00Z", "second")?; append_user_message(rollout_path.as_path(), "2025-01-05T12:02:00Z", "third")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -718,7 +789,7 @@ async fn thread_read_returns_forked_from_id_for_forked_threads() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let fork_id = mcp @@ -758,7 +829,7 @@ async fn thread_read_loaded_thread_returns_precomputed_path_before_materializati let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -818,7 +889,7 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Set a user-facing thread title. @@ -964,7 +1035,7 @@ async fn thread_read_include_turns_rejects_unmaterialized_loaded_thread() -> Res let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -1015,7 +1086,7 @@ async fn thread_turns_list_rejects_unmaterialized_loaded_thread() -> Result<()> let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -1069,7 +1140,7 @@ async fn thread_turns_items_list_returns_unsupported() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -1107,7 +1178,7 @@ async fn thread_read_reports_system_error_idle_flag_after_failed_turn() -> Resul let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -1126,6 +1197,7 @@ async fn thread_read_reports_system_error_idle_flag_after_failed_turn() -> Resul let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "fail this turn".to_string(), text_elements: Vec::new(), @@ -1216,7 +1288,7 @@ fn append_thread_rollback(path: &Path, timestamp: &str, num_turns: u32) -> std:: } async fn read_single_turn_items_view( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, thread_id: &str, items_view: Option, ) -> anyhow::Result { @@ -1285,6 +1357,7 @@ async fn seed_pathless_store_thread( .create_thread(CreateThreadParams { thread_id, forked_from_id: None, + parent_thread_id: None, source: ProtocolSessionSource::Cli, thread_source: None, base_instructions: BaseInstructions::default(), @@ -1294,7 +1367,6 @@ async fn seed_pathless_store_thread( model_provider: "test-provider".to_string(), memory_mode: ThreadMemoryMode::Disabled, }, - event_persistence_mode: ThreadEventPersistenceMode::default(), }) .await?; store @@ -1319,6 +1391,7 @@ async fn seed_pathless_store_thread( fn store_history_items() -> Vec { vec![RolloutItem::EventMsg(EventMsg::UserMessage( UserMessageEvent { + client_id: None, message: "history from store".to_string(), images: None, local_images: Vec::new(), 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 6416499663..ff5d537225 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1,6 +1,6 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_apply_patch_sse_response; use app_test_support::create_fake_rollout; use app_test_support::create_fake_rollout_with_text_elements; @@ -37,18 +37,21 @@ use codex_app_server_protocol::ThreadMetadataGitInfoUpdateParams; use codex_app_server_protocol::ThreadMetadataUpdateParams; use codex_app_server_protocol::ThreadReadParams; use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadResumeInitialTurnsPageParams; use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadSource; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStatus; +use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; use codex_config::types::AuthCredentialsStoreMode; +use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; @@ -143,7 +146,7 @@ async fn thread_resume_rejects_unmaterialized_thread() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a thread. @@ -190,7 +193,7 @@ async fn thread_resume_with_empty_path_uses_running_thread_id() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -209,6 +212,7 @@ async fn thread_resume_with_empty_path_uses_running_thread_id() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize rollout".to_string(), text_elements: Vec::new(), @@ -258,7 +262,7 @@ async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> Resul let extra_root = extra_root_tmp.path().join("extra-root"); std::fs::create_dir_all(&extra_root)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -277,6 +281,7 @@ async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> Resul let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -333,7 +338,7 @@ async fn thread_goal_get_rejects_unmaterialized_thread() -> Result<()> { config.replace("personality = true\n", "personality = true\ngoals = true\n"), )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -398,7 +403,7 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> { "user", )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -421,7 +426,14 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "gpt-5.3-codex", "resumed", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "gpt-5.3-codex", + "resumed", + "user", + ); assert_eq!(event["event_params"]["thread_source"], "user"); Ok(()) } @@ -469,7 +481,7 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -522,48 +534,59 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { #[tokio::test] async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<()> { for client_name in ["codex_chatgpt_android_remote", "codex_chatgpt_ios_remote"] { - let remote_thread = resume_redaction_fixture(Some(client_name)).await?; - let remote_turn = remote_thread + let remote_resume = resume_redaction_fixture(Some(client_name)).await?; + let remote_turn = remote_resume + .thread .turns .first() .expect("remote resume should include a turn"); - let remote_mcp_item = remote_turn - .items - .iter() - .find(|item| matches!(item, ThreadItem::McpToolCall { .. })) - .expect("remote resume should include redacted MCP item"); - let ThreadItem::McpToolCall { - arguments, - result, - error, - .. - } = remote_mcp_item - else { - unreachable!("matched MCP item"); - }; - assert_eq!(arguments, &json!("[redacted]")); - let result = result.as_ref().expect("redacted MCP result"); - assert_eq!( - result.content, - vec![json!({ - "type": "text", - "text": "[redacted]", - })] - ); - assert_eq!(result.structured_content, None); - assert_eq!(result.meta, None); - assert_eq!(error, &None); - assert!( - !remote_turn + let remote_page_turn = remote_resume + .initial_turns_page + .as_ref() + .expect("remote resume should include the requested initial turns page") + .data + .first() + .expect("remote initial turns page should include a turn"); + for remote_turn in [remote_turn, remote_page_turn] { + let remote_mcp_item = remote_turn .items .iter() - .any(|item| matches!(item, ThreadItem::ImageGeneration { .. })), - "remote resume should drop image generation items for {client_name}" - ); + .find(|item| matches!(item, ThreadItem::McpToolCall { .. })) + .expect("remote resume should include redacted MCP item"); + let ThreadItem::McpToolCall { + arguments, + result, + error, + .. + } = remote_mcp_item + else { + unreachable!("matched MCP item"); + }; + assert_eq!(arguments, &json!("[redacted]")); + let result = result.as_ref().expect("redacted MCP result"); + assert_eq!( + result.content, + vec![json!({ + "type": "text", + "text": "[redacted]", + })] + ); + assert_eq!(result.structured_content, None); + assert_eq!(result.meta, None); + assert_eq!(error, &None); + assert!( + !remote_turn + .items + .iter() + .any(|item| matches!(item, ThreadItem::ImageGeneration { .. })), + "remote resume should drop image generation items for {client_name}" + ); + } } - let normal_thread = resume_redaction_fixture(Some("some_other_client")).await?; - let normal_turn = normal_thread + let normal_resume = resume_redaction_fixture(Some("some_other_client")).await?; + let normal_turn = normal_resume + .thread .turns .first() .expect("normal resume should include a turn"); @@ -608,9 +631,7 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<( Ok(()) } -async fn resume_redaction_fixture( - client_name: Option<&str>, -) -> Result { +async fn resume_redaction_fixture(client_name: Option<&str>) -> Result { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; @@ -632,7 +653,7 @@ async fn resume_redaction_fixture( &conversation_id, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; if let Some(client_name) = client_name { let _ = timeout( DEFAULT_READ_TIMEOUT, @@ -650,6 +671,11 @@ async fn resume_redaction_fixture( let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { thread_id: conversation_id, + initial_turns_page: Some(ThreadResumeInitialTurnsPageParams { + limit: None, + sort_direction: None, + items_view: Some(TurnItemsView::Full), + }), ..Default::default() }) .await?; @@ -658,8 +684,7 @@ async fn resume_redaction_fixture( mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), ) .await??; - let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; - Ok(thread) + to_response::(resume_resp) } fn append_resume_redaction_history( @@ -733,7 +758,7 @@ async fn thread_resume_can_skip_turns_for_metadata_only_resume() -> Result<()> { /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -756,6 +781,57 @@ async fn thread_resume_can_skip_turns_for_metadata_only_resume() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_rejects_archived_session_by_id() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let filename_ts = "2025-01-05T12-00-00"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home.path(), + filename_ts, + "2025-01-05T12:00:00Z", + "Archived saved user message", + Vec::new(), + Some("mock_provider"), + /*git_info*/ None, + )?; + let active_rollout_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); + let archived_dir = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR); + std::fs::create_dir_all(&archived_dir)?; + std::fs::rename( + &active_rollout_path, + archived_dir.join(active_rollout_path.file_name().expect("rollout file name")), + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id.clone(), + ..Default::default() + }) + .await?; + let resume_err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(resume_id)), + ) + .await??; + + let message = resume_err.error.message; + assert!( + message.contains(&format!("session {conversation_id} is archived")) + && message.contains(&format!( + "codex unarchive {conversation_id}` to unarchive it first" + )), + "unexpected resume error: {message}" + ); + + Ok(()) +} + #[tokio::test] async fn thread_resume_keeps_paused_goal_paused() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -768,7 +844,7 @@ async fn thread_resume_keeps_paused_goal_paused() -> Result<()> { config.replace("personality = true\n", "personality = true\ngoals = true\n"), )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -787,6 +863,7 @@ async fn thread_resume_keeps_paused_goal_paused() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize this thread".to_string(), text_elements: Vec::new(), @@ -872,7 +949,7 @@ async fn thread_goal_set_preserves_budget_limited_same_objective() -> Result<()> config.replace("personality = true\n", "personality = true\ngoals = true\n"), )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -891,6 +968,7 @@ async fn thread_goal_set_preserves_budget_limited_same_objective() -> Result<()> let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize this thread".to_string(), text_elements: Vec::new(), @@ -970,7 +1048,7 @@ async fn thread_goal_set_persists_resumable_stopped_statuses() -> Result<()> { config.replace("personality = true\n", "personality = true\ngoals = true\n"), )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -989,6 +1067,7 @@ async fn thread_goal_set_persists_resumable_stopped_statuses() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize this thread".to_string(), text_elements: Vec::new(), @@ -1064,7 +1143,7 @@ async fn thread_goal_set_edits_objective_without_resetting_usage() -> Result<()> /*git_info*/ None, )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let goal_id = mcp @@ -1165,7 +1244,7 @@ async fn thread_goal_clear_deletes_goal_and_notifies() -> Result<()> { config.replace("personality = true\n", "personality = true\ngoals = true\n"), )?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -1184,6 +1263,7 @@ async fn thread_goal_clear_deletes_goal_and_notifies() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize this thread".to_string(), text_elements: Vec::new(), @@ -1294,7 +1374,7 @@ async fn thread_resume_emits_restored_token_usage_before_next_turn() -> Result<( Some("mock_provider"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -1347,7 +1427,7 @@ async fn thread_resume_skips_restored_token_usage_when_turns_are_excluded() -> R Some("mock_provider"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let first_resume_id = mcp @@ -1455,7 +1535,7 @@ async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn() format!("{persisted_rollout}{appended_rollout}\n"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -1579,7 +1659,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re format!("{persisted_rollout}{appended_rollout}\n"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -1711,6 +1791,7 @@ stream_max_retries = 0 let session_meta = SessionMeta { id: conversation_id, forked_from_id: None, + parent_thread_id: None, timestamp: "2025-01-05T12:00:00Z".to_string(), cwd: repo_path.clone(), originator: "codex".to_string(), @@ -1767,7 +1848,7 @@ stream_max_retries = 0 .mark_backfill_complete(/*last_watermark*/ None) .await?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let update_id = mcp @@ -1861,7 +1942,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is format!("{persisted_rollout}{appended_rollout}\n"), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -1929,13 +2010,13 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is } #[tokio::test] -async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> { +async fn thread_resume_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 thread_id = rollout.conversation_id.clone(); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let read_id = mcp @@ -1973,6 +2054,33 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; assert_eq!(after_modified, rollout.before_modified); + let unsubscribe_id = mcp + .send_thread_unsubscribe_request(ThreadUnsubscribeParams { + thread_id: thread_id.clone(), + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)), + ) + .await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: "not-a-valid-thread-id".to_string(), + path: Some(normalized_existing_path(&rollout.rollout_file_path)?), + cwd: Some(codex_home.path().to_string_lossy().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 { cwd, .. } = to_response::(resume_resp)?; + assert_eq!(cwd, AbsolutePathBuf::from_absolute_path(codex_home.path())?); + let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id, @@ -2006,7 +2114,7 @@ async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2025,6 +2133,7 @@ async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> { let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2044,12 +2153,13 @@ async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> { .await??; primary.clear_message_buffer(); - let mut secondary = McpProcess::new(codex_home.path()).await?; + let mut secondary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??; let turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "respond with docs".to_string(), text_elements: Vec::new(), @@ -2113,7 +2223,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2132,6 +2242,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2155,6 +2266,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { let running_turn_request_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "keep running".to_string(), text_elements: Vec::new(), @@ -2229,7 +2341,7 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2248,6 +2360,7 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2271,6 +2384,7 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result let running_turn_request_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "keep running".to_string(), text_elements: Vec::new(), @@ -2291,9 +2405,10 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result ) .await??; - let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &thread_id); + let stale_thread_id = Uuid::new_v4().to_string(); + let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &stale_thread_id); std::fs::create_dir_all(stale_path.parent().expect("stale path parent"))?; - let thread_uuid = Uuid::parse_str(&thread_id)?; + let thread_uuid = Uuid::parse_str(&stale_thread_id)?; let mut stale_file = std::fs::File::create(&stale_path)?; let stale_meta = json!({ "timestamp": "2025-01-01T00:00:00Z", @@ -2364,7 +2479,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2383,6 +2498,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2405,6 +2521,7 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R let running_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "keep running".to_string(), text_elements: Vec::new(), @@ -2412,11 +2529,13 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R ..Default::default() }) .await?; - timeout( + let running_turn_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)), ) .await??; + let TurnStartResponse { turn: running_turn } = + to_response::(running_turn_resp)?; timeout( DEFAULT_READ_TIMEOUT, primary.read_stream_until_notification_message("turn/started"), @@ -2428,6 +2547,11 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R thread_id: thread.id.clone(), model: Some("not-the-running-model".to_string()), cwd: Some("/tmp".to_string()), + initial_turns_page: Some(ThreadResumeInitialTurnsPageParams { + limit: None, + sort_direction: None, + items_view: None, + }), ..Default::default() }) .await?; @@ -2436,9 +2560,23 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R primary.read_stream_until_response_message(RequestId::Integer(resume_id)), ) .await??; - let ThreadResumeResponse { thread, model, .. } = - to_response::(resume_resp)?; + let ThreadResumeResponse { + thread, + model, + initial_turns_page, + .. + } = to_response::(resume_resp)?; assert_eq!(model, "gpt-5.4"); + let initial_turns_page = initial_turns_page.expect("resume should include initial turns page"); + let resumed_running_turn = initial_turns_page + .data + .first() + .expect("resume page should include the running turn"); + assert_eq!(resumed_running_turn.id, running_turn.id); + assert_eq!(resumed_running_turn.items_view, TurnItemsView::Summary); + assert_eq!(resumed_running_turn.status, TurnStatus::InProgress); + assert!(initial_turns_page.backwards_cursor.is_some()); + assert_eq!(initial_turns_page.next_cursor, None); // The running-thread resume response is queued onto the thread listener task. // If the in-flight turn completes before that queued command runs, the response // can legitimately observe the thread as idle. @@ -2472,7 +2610,7 @@ async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2491,6 +2629,7 @@ async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> { let turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2509,7 +2648,7 @@ async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> { ) .await??; - let mut secondary = McpProcess::new(codex_home.path()).await?; + let mut secondary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??; let resume_id = secondary @@ -2555,7 +2694,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2574,6 +2713,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2596,6 +2736,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R let running_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "run command".to_string(), text_elements: Vec::new(), @@ -2692,7 +2833,7 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result< let server = create_mock_responses_server_sequence_unchecked(responses).await; create_config_toml(&codex_home, &server.uri())?; - let mut primary = McpProcess::new(&codex_home).await?; + let mut primary = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -2712,6 +2853,7 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result< let seed_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -2735,6 +2877,7 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result< let running_turn_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "apply patch".to_string(), text_elements: Vec::new(), @@ -2879,6 +3022,7 @@ async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Re let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: resumed_thread.id, + client_user_message_id: None, input: vec![UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -2910,7 +3054,7 @@ async fn thread_resume_fails_when_required_mcp_server_fails_to_initialize() -> R let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; create_config_toml_with_required_broken_mcp(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp @@ -2989,7 +3133,7 @@ async fn thread_resume_surfaces_cloud_requirements_load_errors() -> Result<()> { /*git_info*/ None, )?; let refresh_token_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -3083,7 +3227,7 @@ async fn thread_resume_can_load_source_by_external_path() -> Result<()> { )?; let thread_path = rollout_path(external_home.path(), "2025-01-05T12-00-00", &thread_id); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { @@ -3162,7 +3306,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> { } struct RestartedThreadFixture { - mcp: McpProcess, + mcp: TestAppServer, thread_id: String, rollout_file_path: PathBuf, updated_at: i64, @@ -3172,7 +3316,7 @@ async fn start_materialized_thread_and_restart( codex_home: &Path, seed_text: &str, ) -> Result { - let mut first_mcp = McpProcess::new(codex_home).await?; + let mut first_mcp = TestAppServer::new(codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, first_mcp.initialize()).await??; let start_id = first_mcp @@ -3191,6 +3335,7 @@ async fn start_materialized_thread_and_restart( let materialize_turn_id = first_mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: seed_text.to_string(), text_elements: Vec::new(), @@ -3230,7 +3375,7 @@ async fn start_materialized_thread_and_restart( drop(first_mcp); - let mut second_mcp = McpProcess::new(codex_home).await?; + let mut second_mcp = TestAppServer::new(codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, second_mcp.initialize()).await??; Ok(RestartedThreadFixture { @@ -3261,7 +3406,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut primary = McpProcess::new(codex_home.path()).await?; + let mut primary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??; let start_id = primary @@ -3280,6 +3425,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> { let materialize_id = primary .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "seed history".to_string(), text_elements: Vec::new(), @@ -3298,7 +3444,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> { ) .await??; - let mut secondary = McpProcess::new(codex_home.path()).await?; + let mut secondary = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??; let resume_id = secondary @@ -3320,6 +3466,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> { let turn_id = secondary .send_turn_start_request(TurnStartParams { thread_id: resume.thread.id, + client_user_message_id: None, input: vec![UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs b/codex-rs/app-server/tests/suite/v2/thread_rollback.rs index 5f79db0e26..4f45aa496b 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_rollback.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; @@ -35,7 +35,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a thread. @@ -57,6 +57,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() let turn1_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: first_text.to_string(), text_elements: Vec::new(), @@ -78,6 +79,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() let turn2_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Second".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs b/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs index aef2e961c0..98ad587326 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_settings_update.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; @@ -44,7 +44,7 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() -> write_models_cache(codex_home.path())?; let (model_id, service_tier_id) = service_tier_model_and_tier_id()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread = start_thread(&mut mcp).await?.thread; @@ -104,7 +104,7 @@ async fn thread_settings_update_while_turn_is_active_emits_notification() -> Res let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread = start_thread(&mut mcp).await?.thread; start_text_turn(&mut mcp, thread.id.clone()).await?; @@ -147,7 +147,7 @@ async fn thread_settings_update_null_service_tier_uses_default() -> Result<()> { write_models_cache(codex_home.path())?; let (model_id, service_tier_id) = service_tier_model_and_tier_id()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread = start_thread(&mut mcp).await?.thread; @@ -213,7 +213,7 @@ async fn thread_settings_update_rejects_sandbox_policy_with_permissions() -> Res let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread = start_thread(&mut mcp).await?.thread; @@ -247,7 +247,7 @@ async fn turn_start_settings_override_emits_thread_settings_updated() -> Result< let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let thread = start_thread(&mut mcp).await?.thread; timeout( @@ -259,6 +259,7 @@ async fn turn_start_settings_override_emits_thread_settings_updated() -> Result< let turn_request_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "hello".to_string(), text_elements: Vec::new(), @@ -288,7 +289,7 @@ async fn turn_start_settings_override_emits_thread_settings_updated() -> Result< } async fn send_thread_settings_update( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, params: ThreadSettingsUpdateParams, ) -> Result<()> { let request_id = mcp.send_thread_settings_update_request(params).await?; @@ -301,7 +302,7 @@ async fn send_thread_settings_update( Ok(()) } -async fn start_text_turn(mcp: &mut McpProcess, thread_id: String) -> Result<()> { +async fn start_text_turn(mcp: &mut TestAppServer, thread_id: String) -> Result<()> { let turn_request_id = mcp .send_turn_start_request(TurnStartParams { thread_id, @@ -322,7 +323,7 @@ async fn start_text_turn(mcp: &mut McpProcess, thread_id: String) -> Result<()> Ok(()) } -async fn start_thread(mcp: &mut McpProcess) -> Result { +async fn start_thread(mcp: &mut TestAppServer) -> Result { let request_id = mcp .send_thread_start_request(ThreadStartParams { model: Some("mock-model".to_string()), @@ -338,7 +339,7 @@ async fn start_thread(mcp: &mut McpProcess) -> Result { } async fn read_thread_with_turns( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, thread_id: &str, ) -> Result { let request_id = mcp @@ -356,7 +357,7 @@ async fn read_thread_with_turns( } async fn read_thread_settings_updated( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, ) -> Result { let notification: JSONRPCNotification = timeout( DEFAULT_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs index 4ab677aca1..312267d0a6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_shell_command.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_shell_command_sse_response; @@ -60,14 +60,11 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.as_path()).await?; + let mut mcp = TestAppServer::new(codex_home.as_path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp - .send_thread_start_request(ThreadStartParams { - persist_extended_history: true, - ..Default::default() - }) + .send_thread_start_request(ThreadStartParams::default()) .await?; let start_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -196,7 +193,7 @@ async fn thread_shell_command_returns_error_when_local_environment_is_disabled() &BTreeMap::default(), )?; - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.as_path(), &[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))], ) @@ -255,14 +252,11 @@ async fn thread_shell_command_uses_existing_active_turn() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.as_path()).await?; + let mut mcp = TestAppServer::new(codex_home.as_path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp - .send_thread_start_request(ThreadStartParams { - persist_extended_history: true, - ..Default::default() - }) + .send_thread_start_request(ThreadStartParams::default()) .await?; let start_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, @@ -275,6 +269,7 @@ async fn thread_shell_command_uses_existing_active_turn() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), @@ -409,7 +404,7 @@ fn current_shell_output_command(text: &str) -> Result<(String, String)> { } async fn wait_for_command_execution_started( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, expected_id: Option<&str>, ) -> Result { loop { @@ -431,7 +426,7 @@ async fn wait_for_command_execution_started( } async fn wait_for_command_execution_started_by_source( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, expected_source: CommandExecutionSource, ) -> Result { loop { @@ -446,7 +441,7 @@ async fn wait_for_command_execution_started_by_source( } async fn wait_for_command_execution_completed( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, expected_id: Option<&str>, ) -> Result { loop { @@ -468,7 +463,7 @@ async fn wait_for_command_execution_completed( } async fn wait_for_command_execution_output_delta( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, item_id: &str, ) -> Result { loop { diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index ecd8ad8e3d..3c4a0dba5e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -1,12 +1,11 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; -use app_test_support::McpProcess; use app_test_support::PathBufExt; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::AskForApproval; -use codex_app_server_protocol::DeprecationNoticeNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::JSONRPCResponse; @@ -52,46 +51,6 @@ use super::analytics::wait_for_analytics_payload; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const INVALID_REQUEST_ERROR_CODE: i64 = -32600; -#[tokio::test] -async fn thread_start_deprecates_persist_extended_history_true() -> Result<()> { - let server = create_mock_responses_server_repeating_assistant("Done").await; - let codex_home = TempDir::new()?; - create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let req_id = mcp - .send_thread_start_request(ThreadStartParams { - persist_extended_history: true, - ..Default::default() - }) - .await?; - - let notification = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("deprecationNotice"), - ) - .await??; - let notice: DeprecationNoticeNotification = serde_json::from_value( - notification - .params - .expect("deprecationNotice params should be present"), - )?; - assert_eq!( - notice.summary, - "persistExtendedHistory is deprecated and ignored" - ); - - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(req_id)), - ) - .await??; - - Ok(()) -} - #[tokio::test] async fn thread_start_creates_thread_and_emits_started() -> Result<()> { // Provide a mock server and config so model wiring is valid. @@ -101,7 +60,7 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> { create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; // Start server and initialize. - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a v2 thread with an explicit model override. @@ -248,7 +207,7 @@ async fn thread_start_resolves_runtime_workspace_roots_against_cwd() -> Result<( let relative_root = PathBuf::from("extra-root"); std::fs::create_dir_all(cwd.join(&relative_root))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -292,7 +251,7 @@ async fn thread_start_excludes_profile_workspace_roots_from_runtime_workspace_ro profile_root.path(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -327,7 +286,7 @@ async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result let codex_home = TempDir::new()?; create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -364,7 +323,7 @@ async fn thread_start_response_includes_loaded_instruction_sources() -> Result<( let project_agents_path = workspace.path().join("AGENTS.md"); std::fs::write(&project_agents_path, "project instructions")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -420,7 +379,7 @@ async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> { create_config_toml_with_chatgpt_base_url(codex_home.path(), &server.uri(), &server.uri())?; mount_analytics_capture(&server, codex_home.path()).await?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -439,7 +398,14 @@ async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; assert_eq!(payload["events"].as_array().expect("events array").len(), 1); let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "mock-model", "new", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "mock-model", + "new", + "user", + ); Ok(()) } @@ -461,7 +427,7 @@ model_reasoning_effort = "high" )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -491,7 +457,7 @@ async fn thread_start_drops_unsupported_service_tier_id() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let service_tier_id = "experimental-tier-id".to_string(); @@ -521,7 +487,7 @@ async fn thread_start_accepts_default_service_tier() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -552,7 +518,7 @@ async fn thread_start_accepts_metrics_service_name() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -579,7 +545,7 @@ async fn thread_start_ephemeral_remains_pathless() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -625,7 +591,7 @@ async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Re let codex_home = TempDir::new()?; create_config_toml_with_required_broken_mcp(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -661,7 +627,7 @@ async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result< let codex_home = TempDir::new()?; create_config_toml_with_optional_broken_mcp(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let req_id = mcp @@ -791,7 +757,7 @@ async fn thread_start_surfaces_cloud_requirements_load_errors() -> Result<()> { )?; let refresh_token_url = format!("{}/oauth/token", server.uri()); - let mut mcp = McpProcess::new_with_env( + let mut mcp = TestAppServer::new_with_env( codex_home.path(), &[ ("OPENAI_API_KEY", None), @@ -851,7 +817,7 @@ model_reasoning_effort = "high" "#, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let first_request = mcp @@ -911,7 +877,7 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> { let nested = repo_root.path().join("nested/project"); std::fs::create_dir_all(&nested)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -949,7 +915,7 @@ async fn thread_start_with_read_only_sandbox_does_not_persist_project_trust() -> let workspace = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -987,7 +953,7 @@ async fn thread_start_preserves_untrusted_project_trust() -> Result<()> { std::fs::write(&config_path, config_toml.to_string())?; let config_before = std::fs::read_to_string(&config_path)?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -1028,7 +994,7 @@ model_reasoning_effort = "high" set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; let config_before = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/app-server/tests/suite/v2/thread_status.rs b/codex-rs/app-server/tests/suite/v2/thread_status.rs index 957969c3ea..b8349a77f1 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_status.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_status.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::to_response; @@ -29,7 +29,7 @@ async fn thread_status_changed_emits_runtime_updates() -> Result<()> { create_config_toml(codex_home.path(), &server.uri())?; let mut mcp = - McpProcess::new_with_env(codex_home.path(), &[("RUST_LOG", Some("info"))]).await?; + TestAppServer::new_with_env(codex_home.path(), &[("RUST_LOG", Some("info"))]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_start_id = mcp @@ -48,6 +48,7 @@ async fn thread_status_changed_emits_runtime_updates() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "collect status updates".to_string(), text_elements: Vec::new(), @@ -134,7 +135,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { let server = create_mock_responses_server_sequence(responses).await; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; let message = timeout( DEFAULT_READ_TIMEOUT, mcp.initialize_with_capabilities( @@ -171,6 +172,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> { let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run once".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index d2f8f268a2..70a9ff23de 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use codex_app_server::in_process; @@ -35,7 +35,6 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::ThreadMemoryMode; use codex_thread_store::CreateThreadParams; use codex_thread_store::InMemoryThreadStore; -use codex_thread_store::ThreadEventPersistenceMode; use codex_thread_store::ThreadMetadataPatch; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; @@ -60,7 +59,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -81,6 +80,7 @@ async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result let turn_start_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![UserInput::Text { text: "materialize".to_string(), text_elements: Vec::new(), @@ -210,6 +210,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { .create_thread(CreateThreadParams { thread_id, forked_from_id: Some(parent_thread_id), + parent_thread_id: None, source: SessionSource::Cli, thread_source: None, base_instructions: BaseInstructions::default(), @@ -219,7 +220,6 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { model_provider: "test-provider".to_string(), memory_mode: ThreadMemoryMode::Disabled, }, - event_persistence_mode: ThreadEventPersistenceMode::default(), }) .await?; store diff --git a/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs index c0188add8c..55aac670f8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::to_response; use codex_app_server_protocol::DynamicToolCallOutputContentItem; @@ -41,7 +41,7 @@ async fn thread_unsubscribe_keeps_thread_loaded_until_idle_timeout() -> Result<( let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -120,7 +120,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> { let final_response_completed = completions.remove(0); create_config_toml(&codex_home, server.uri())?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -151,6 +151,7 @@ async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run deterministic tool".to_string(), text_elements: Vec::new(), @@ -252,7 +253,7 @@ async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Resu let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -260,6 +261,7 @@ async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Resu let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread_id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "fail this turn".to_string(), text_elements: Vec::new(), @@ -317,6 +319,7 @@ async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Resu let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { thread_id, + cwd: Some(codex_home.path().to_string_lossy().to_string()), ..Default::default() }) .await?; @@ -337,7 +340,7 @@ async fn thread_unsubscribe_reports_not_subscribed_before_idle_unload() -> Resul let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_id = start_thread(&mut mcp).await?; @@ -376,7 +379,7 @@ async fn thread_unsubscribe_reports_not_subscribed_before_idle_unload() -> Resul } async fn wait_for_dynamic_tool_started( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, call_id: &str, ) -> Result { loop { @@ -416,7 +419,7 @@ stream_max_retries = 0 ) } -async fn start_thread(mcp: &mut McpProcess) -> Result { +async fn start_thread(mcp: &mut TestAppServer) -> Result { let req_id = mcp .send_thread_start_request(ThreadStartParams { model: Some("mock-model".to_string()), diff --git a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs index aedc54e016..335d32776d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs @@ -1,7 +1,7 @@ #![cfg(unix)] use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; @@ -57,7 +57,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> { .await; create_config_toml(&codex_home, &server.uri(), "never", "workspace-write")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a v2 thread and capture its id. @@ -78,6 +78,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run sleep".to_string(), text_elements: Vec::new(), @@ -140,7 +141,7 @@ async fn turn_interrupt_rejects_completed_turn() -> Result<()> { .await; create_config_toml(&codex_home, &server.uri(), "never", "workspace-write")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -159,6 +160,7 @@ async fn turn_interrupt_rejects_completed_turn() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "say done".to_string(), text_elements: Vec::new(), @@ -234,7 +236,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<() .await; create_config_toml(&codex_home, &server.uri(), "untrusted", "read-only")?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -253,6 +255,7 @@ async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<() let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 236d57a0ee..0c2c7b71cd 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1,7 +1,7 @@ use anyhow::Context; use anyhow::Result; use app_test_support::DEFAULT_CLIENT_NAME; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_apply_patch_sse_response; use app_test_support::create_exec_command_sse_response; use app_test_support::create_fake_rollout; @@ -16,6 +16,8 @@ use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url; use app_test_support::write_models_cache; use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; use codex_app_server::INVALID_PARAMS_ERROR_CODE; +use codex_app_server_protocol::AdditionalContextEntry; +use codex_app_server_protocol::AdditionalContextKind; use codex_app_server_protocol::ByteRange; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::CollabAgentStatus; @@ -117,7 +119,7 @@ async fn run_local_image_turn(detail: Option) -> Result> &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -139,6 +141,7 @@ async fn run_local_image_turn(detail: Option) -> Result> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::LocalImage { path: image_path, detail, @@ -213,7 +216,7 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -233,6 +236,7 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: Vec::new(), ..Default::default() }) @@ -301,6 +305,83 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_additional_context_flows_to_model_input() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "inspect tab".to_string(), + text_elements: Vec::new(), + }], + additional_context: Some(HashMap::from([( + "custom_source".to_string(), + AdditionalContextEntry { + value: "source value".to_string(), + kind: AdditionalContextKind::Untrusted, + }, + )])), + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = server + .received_requests() + .await + .context("failed to fetch received requests")?; + let request = requests + .iter() + .find(|request| request.url.path().ends_with("/responses")) + .context("expected model request")?; + let body = request + .body_json::() + .context("request body should be JSON")?; + assert!( + body.to_string() + .contains("source value") + ); + + Ok(()) +} + #[tokio::test] async fn turn_start_sends_originator_header() -> Result<()> { let responses = vec![create_final_assistant_message_sse_response("Done")?]; @@ -314,7 +395,7 @@ async fn turn_start_sends_originator_header() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout( DEFAULT_READ_TIMEOUT, mcp.initialize_with_client_info(ClientInfo { @@ -342,6 +423,7 @@ async fn turn_start_sends_originator_header() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -390,7 +472,7 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -414,6 +496,7 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: Some("client-message-1".to_string()), input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: text_elements.clone(), @@ -443,7 +526,10 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { .await??; match user_message_item { - ThreadItem::UserMessage { content, .. } => { + ThreadItem::UserMessage { + client_id, content, .. + } => { + assert_eq!(client_id, Some("client-message-1".to_string())); assert_eq!( content, vec![V2UserInput::Text { @@ -501,7 +587,7 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills( write_test_skill(codex_home.path(), "alpha-skill")?; write_test_skill(codex_home.path(), "beta-skill")?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -517,6 +603,7 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills( let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -593,7 +680,7 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> { .expect("bundled model catalog should include a picker model with service tiers"); let service_tier_id = service_tier_model.service_tiers[0].id.clone(); - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -657,7 +744,7 @@ async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -683,6 +770,7 @@ async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -742,7 +830,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { )?; mount_analytics_capture(&server, codex_home.path()).await?; - let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?; + let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -762,6 +850,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Image { url: "https://example.com/a.png".to_string(), detail: None, @@ -784,6 +873,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_event").await?; assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["session_id"], thread.session_id); assert_eq!(event["event_params"]["turn_id"], turn.id); assert_eq!( event["event_params"]["app_server_client"]["product_client_id"], @@ -830,7 +920,7 @@ async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -849,6 +939,7 @@ async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![ V2UserInput::Text { text: "x".repeat(MAX_USER_INPUT_TEXT_CHARS), @@ -889,7 +980,7 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -912,6 +1003,7 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![ V2UserInput::Text { text: first, @@ -968,7 +1060,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() "sandbox_mode = \"read-only\"\n", )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -986,6 +1078,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1039,7 +1132,7 @@ async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -1058,6 +1151,7 @@ async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1110,7 +1204,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // Start a thread (v2) and capture its id. @@ -1131,6 +1225,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1183,6 +1278,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( let turn_req2 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Second".to_string(), text_elements: Vec::new(), @@ -1253,7 +1349,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -1281,6 +1377,7 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1338,7 +1435,7 @@ async fn turn_start_uses_thread_feature_overrides_for_request_user_input_tool_de &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -1370,6 +1467,7 @@ async fn turn_start_uses_thread_feature_overrides_for_request_user_input_tool_de let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1422,7 +1520,7 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -1441,6 +1539,7 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1503,7 +1602,7 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { &BTreeMap::from([(Feature::Personality, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -1522,6 +1621,7 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1546,6 +1646,7 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { let turn_req2 = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello again".to_string(), text_elements: Vec::new(), @@ -1617,7 +1718,7 @@ async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() -> /*git_info*/ None, )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let persisted_toml: ConfigToml = toml::from_str(&std::fs::read_to_string( @@ -1648,6 +1749,7 @@ async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() -> let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -1747,7 +1849,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.as_path()).await?; + let mut mcp = TestAppServer::new(codex_home.as_path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // thread/start @@ -1768,6 +1870,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { let first_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), @@ -1832,6 +1935,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { let second_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python again".to_string(), text_elements: Vec::new(), @@ -1890,7 +1994,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(codex_home.as_path()).await?; + let mut mcp = TestAppServer::new(codex_home.as_path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -1909,6 +2013,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), @@ -2042,7 +2147,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; // thread/start @@ -2064,11 +2169,13 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { .send_turn_start_request(TurnStartParams { environments: None, thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "first turn".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, cwd: Some(first_cwd.clone()), runtime_workspace_roots: None, approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), @@ -2106,11 +2213,13 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { .send_turn_start_request(TurnStartParams { environments: None, thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, cwd: Some(second_cwd.clone()), runtime_workspace_roots: None, approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), @@ -2228,7 +2337,7 @@ stream_max_retries = 0 ), )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -2247,6 +2356,7 @@ stream_max_retries = 0 let first_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "select dev profile".to_string(), text_elements: Vec::new(), @@ -2270,6 +2380,7 @@ stream_max_retries = 0 let second_turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "write in new root".to_string(), text_elements: Vec::new(), @@ -2337,7 +2448,7 @@ url = "ws://127.0.0.1:1" "#, )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; for case in [ @@ -2380,7 +2491,7 @@ struct EnvironmentSelectionCase { } async fn run_environment_selection_case( - mcp: &mut McpProcess, + mcp: &mut TestAppServer, workspace: &Path, case: EnvironmentSelectionCase, ) -> Result<()> { @@ -2402,6 +2513,7 @@ async fn run_environment_selection_case( let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: format!("run {}", case.name), text_elements: Vec::new(), @@ -2497,7 +2609,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_req = mcp @@ -2517,6 +2629,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -2687,7 +2800,7 @@ async fn turn_start_does_not_stream_apply_patch_change_updates_without_feature_v let server = create_mock_responses_server_sequence(responses).await; create_config_toml(&codex_home, &server.uri(), "never", &BTreeMap::default())?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_req = mcp @@ -2707,6 +2820,7 @@ async fn turn_start_does_not_stream_apply_patch_change_updates_without_feature_v let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id, + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -2824,7 +2938,7 @@ async fn turn_start_streams_apply_patch_change_updates_v2() -> Result<()> { model["apply_patch_tool_type"] = serde_json::Value::from("freeform"); std::fs::write(&cache_path, serde_json::to_string_pretty(&cache)?)?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_req = mcp @@ -2844,6 +2958,7 @@ async fn turn_start_streams_apply_patch_change_updates_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -2955,7 +3070,7 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<() &BTreeMap::from([(Feature::Collab, true)]), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -2974,6 +3089,7 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<() let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: PARENT_PROMPT.to_string(), text_elements: Vec::new(), @@ -3174,7 +3290,7 @@ config_file = "./custom-role.toml" ), )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -3193,6 +3309,7 @@ config_file = "./custom-role.toml" let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: PARENT_PROMPT.to_string(), text_elements: Vec::new(), @@ -3317,7 +3434,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_req = mcp @@ -3338,6 +3455,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res let turn_1_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch 1".into(), text_elements: Vec::new(), @@ -3410,6 +3528,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res let turn_2_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch 2".into(), text_elements: Vec::new(), @@ -3488,7 +3607,7 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> { &BTreeMap::default(), )?; - let mut mcp = McpProcess::new(&codex_home).await?; + let mut mcp = TestAppServer::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_req = mcp @@ -3508,6 +3627,7 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "apply patch".into(), text_elements: Vec::new(), @@ -3634,7 +3754,7 @@ async fn command_execution_notifications_include_process_id() -> Result<()> { "danger-full-access", )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let start_id = mcp @@ -3653,6 +3773,7 @@ async fn command_execution_notifications_include_process_id() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run a command".to_string(), text_elements: Vec::new(), @@ -3767,7 +3888,7 @@ async fn turn_start_with_elevated_override_does_not_persist_project_trust() -> R let workspace = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_request = mcp diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index 7b7a6c2563..dfb76ecbc2 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -7,7 +7,7 @@ // network access are required the first time the artifact is fetched. use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; @@ -117,6 +117,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run echo hi".to_string(), text_elements: Vec::new(), @@ -235,6 +236,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), @@ -367,6 +369,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> { let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run python".to_string(), text_elements: Vec::new(), @@ -525,18 +528,18 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() let turn_id = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "remove both files".to_string(), text_elements: Vec::new(), }], cwd: Some(workspace.clone()), approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted), - sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![workspace.clone().try_into()?], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }), + // This test is about execve-intercept approval propagation, not + // workspace sandboxing. Using full access avoids macOS sandbox + // setup failures that can terminate the parent shell before the + // second subcommand approval is observed. + sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess), model: Some("mock-model".to_string()), effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium), summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), @@ -740,10 +743,10 @@ async fn create_zsh_test_mcp_process( codex_home: &Path, zdotdir: &Path, zsh_path: &Path, -) -> Result { +) -> Result { let app_server = create_test_package_app_server(codex_home, zsh_path)?; let zdotdir = zdotdir.to_string_lossy().into_owned(); - McpProcess::new_with_program_and_env( + TestAppServer::new_with_program_and_env( codex_home, &app_server, &[("ZDOTDIR", Some(zdotdir.as_str()))], diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index a92b2db528..ed0ea2def5 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -1,7 +1,8 @@ #![cfg(unix)] +use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; @@ -9,10 +10,14 @@ use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url; use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; use codex_app_server::INVALID_PARAMS_ERROR_CODE; +use codex_app_server_protocol::AdditionalContextEntry; +use codex_app_server_protocol::AdditionalContextKind; +use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; @@ -21,6 +26,8 @@ use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; +use serde_json::Value; +use std::collections::HashMap; use tempfile::TempDir; use tokio::time::timeout; @@ -43,7 +50,7 @@ async fn turn_steer_requires_active_turn() -> Result<()> { )?; mount_analytics_capture(&server, &codex_home).await?; - let mut mcp = McpProcess::new_without_managed_config(&codex_home).await?; + let mut mcp = TestAppServer::new_without_managed_config(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -62,11 +69,13 @@ async fn turn_steer_requires_active_turn() -> Result<()> { let steer_req = mcp .send_turn_steer_request(TurnSteerParams { thread_id: thread.id.clone(), + client_user_message_id: Some("client-steer-message-1".to_string()), input: vec![V2UserInput::Text { text: "steer".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, expected_turn_id: "turn-does-not-exist".to_string(), }) .await?; @@ -127,7 +136,7 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> { )?; mount_analytics_capture(&server, &codex_home).await?; - let mut mcp = McpProcess::new_without_managed_config(&codex_home).await?; + let mut mcp = TestAppServer::new_without_managed_config(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -146,6 +155,7 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run sleep".to_string(), text_elements: Vec::new(), @@ -171,11 +181,13 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> { let steer_req = mcp .send_turn_steer_request(TurnSteerParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: oversized_input.clone(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, expected_turn_id: turn.id.clone(), }) .await?; @@ -210,10 +222,10 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let shell_command = vec![ "powershell".to_string(), "-Command".to_string(), - "Start-Sleep -Seconds 10".to_string(), + "Start-Sleep -Seconds 2".to_string(), ]; #[cfg(not(target_os = "windows"))] - let shell_command = vec!["sleep".to_string(), "10".to_string()]; + let shell_command = vec!["sleep".to_string(), "2".to_string()]; let tmp = TempDir::new()?; let codex_home = tmp.path().join("codex_home"); @@ -221,14 +233,16 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let working_directory = tmp.path().join("workdir"); std::fs::create_dir(&working_directory)?; - let server = - create_mock_responses_server_sequence_unchecked(vec![create_shell_command_sse_response( + let server = create_mock_responses_server_sequence_unchecked(vec![ + create_shell_command_sse_response( shell_command.clone(), Some(&working_directory), Some(10_000), "call_sleep", - )?]) - .await; + )?, + app_test_support::create_final_assistant_message_sse_response("Done")?, + ]) + .await; write_mock_responses_config_toml_with_chatgpt_base_url( &codex_home, &server.uri(), @@ -236,7 +250,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { )?; mount_analytics_capture(&server, &codex_home).await?; - let mut mcp = McpProcess::new_without_managed_config(&codex_home).await?; + let mut mcp = TestAppServer::new_without_managed_config(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let thread_req = mcp @@ -255,6 +269,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), + client_user_message_id: None, input: vec![V2UserInput::Text { text: "run sleep".to_string(), text_elements: Vec::new(), @@ -279,11 +294,13 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let steer_req = mcp .send_turn_steer_request(TurnSteerParams { thread_id: thread.id.clone(), + client_user_message_id: Some("client-steer-message-1".to_string()), input: vec![V2UserInput::Text { text: "steer".to_string(), text_elements: Vec::new(), }], responsesapi_client_metadata: None, + additional_context: None, expected_turn_id: turn.id.clone(), }) .await?; @@ -295,9 +312,38 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let steer: TurnSteerResponse = to_response::(steer_resp)?; assert_eq!(steer.turn_id, turn.id); + timeout(DEFAULT_READ_TIMEOUT, async { + loop { + let notification = mcp + .read_stream_until_notification_message("item/started") + .await?; + let params = notification.params.expect("item/started params"); + let item_started: ItemStartedNotification = + serde_json::from_value(params).expect("deserialize item/started notification"); + let ThreadItem::UserMessage { + client_id, content, .. + } = item_started.item + else { + continue; + }; + if client_id == Some("client-steer-message-1".to_string()) { + assert_eq!( + content, + vec![V2UserInput::Text { + text: "steer".to_string(), + text_elements: Vec::new(), + }] + ); + return Ok::<(), anyhow::Error>(()); + } + } + }) + .await??; + let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_steer_event").await?; assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["session_id"], thread.session_id); assert_eq!(event["event_params"]["result"], "accepted"); assert_eq!(event["event_params"]["num_input_images"], 0); assert_eq!(event["event_params"]["expected_turn_id"], turn.id); @@ -307,8 +353,128 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { serde_json::Value::Null ); - mcp.interrupt_turn_and_wait_for_aborted(thread.id, steer.turn_id, DEFAULT_READ_TIMEOUT) - .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + Ok(()) +} + +#[tokio::test] +async fn turn_steer_rejects_context_only_input_without_merging_context() -> Result<()> { + let tmp = TempDir::new()?; + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir(&codex_home)?; + let working_directory = tmp.path().join("workdir"); + std::fs::create_dir(&working_directory)?; + + let server = create_mock_responses_server_sequence_unchecked(vec![ + create_shell_command_sse_response( + vec!["sleep".to_string(), "1".to_string()], + Some(&working_directory), + Some(10_000), + "call_sleep", + )?, + app_test_support::create_final_assistant_message_sse_response("Done")?, + ]) + .await; + write_mock_responses_config_toml_with_chatgpt_base_url( + &codex_home, + &server.uri(), + &server.uri(), + )?; + mount_analytics_capture(&server, &codex_home).await?; + + let mut mcp = TestAppServer::new_without_managed_config(&codex_home).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "run sleep".to_string(), + text_elements: Vec::new(), + }], + cwd: Some(working_directory), + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let TurnStartResponse { turn } = to_response::(turn_resp)?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/started"), + ) + .await??; + + let additional_context = Some(HashMap::from([( + "browser_info".to_string(), + AdditionalContextEntry { + value: "tab one".to_string(), + kind: AdditionalContextKind::Untrusted, + }, + )])); + let steer_req = mcp + .send_turn_steer_request(TurnSteerParams { + thread_id: thread.id.clone(), + client_user_message_id: None, + input: Vec::new(), + responsesapi_client_metadata: None, + additional_context, + expected_turn_id: turn.id, + }) + .await?; + let steer_error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(steer_req)), + ) + .await??; + assert_eq!(steer_error.error.code, -32600); + assert_eq!(steer_error.error.message, "input must not be empty"); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = server + .received_requests() + .await + .context("failed to fetch received requests")?; + let response_requests = requests + .iter() + .filter(|request| request.url.path().ends_with("/responses")) + .collect::>(); + assert_eq!(response_requests.len(), 2); + let body = response_requests[1] + .body_json::() + .context("request body should be JSON")?; + assert!( + !body + .to_string() + .contains("tab one") + ); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/web_search.rs b/codex-rs/app-server/tests/suite/v2/web_search.rs new file mode 100644 index 0000000000..13c595d148 --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/web_search.rs @@ -0,0 +1,315 @@ +use std::path::Path; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use app_test_support::ChatGptAuthFixture; +use app_test_support::TestAppServer; +use app_test_support::to_response; +use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::ItemCompletedNotification; +use codex_app_server_protocol::ItemStartedNotification; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadItem; +use codex_app_server_protocol::ThreadReadParams; +use codex_app_server_protocol::ThreadReadResponse; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use codex_app_server_protocol::WebSearchAction; +use codex_config::types::AuthCredentialsStoreMode; +use core_test_support::responses; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::timeout; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +// macOS and Windows Bazel CI can spend tens of seconds starting app-server +// subprocesses or processing test RPCs under load. +#[cfg(any(target_os = "macos", windows))] +const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60); +#[cfg(not(any(target_os = "macos", windows)))] +const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); + +#[tokio::test] +async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> { + let call_id = "web-run-1"; + let server = responses::start_mock_server().await; + mount_search_response(&server).await; + + let response_mock = responses::mount_sse_sequence( + &server, + vec![ + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call_with_namespace( + call_id, + "web", + "run", + &json!({ + "search_query": [{"q": "standalone web search"}], + }) + .to_string(), + ), + responses::ev_completed("resp-1"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-2"), + ]), + ], + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("access-chatgpt"), + AuthCredentialsStoreMode::File, + )?; + + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + let thread_id = thread.id.clone(); + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread_id.clone(), + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "Search the web".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _turn: TurnStartResponse = to_response::(turn_resp)?; + + let started = timeout(DEFAULT_READ_TIMEOUT, wait_for_web_search_started(&mut mcp)).await??; + let completed = timeout( + DEFAULT_READ_TIMEOUT, + wait_for_web_search_completed(&mut mcp), + ) + .await??; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = response_mock.requests(); + assert_eq!(requests.len(), 2); + + let first_response = requests[0].body_json(); + let web_run = requests[0] + .tool_by_name("web", "run") + .context("web.run should be sent to the model")?; + assert_eq!( + web_run.pointer("/parameters/properties/time/description"), + Some(&json!("Get time for the given UTC offsets.")) + ); + assert!( + !has_hosted_web_search(&first_response), + "standalone web search should replace hosted web search" + ); + + let search_body = search_request_body(&server).await?; + assert_eq!(search_body["model"], json!("mock-model")); + assert_eq!( + search_body["commands"], + json!({ + "search_query": [{"q": "standalone web search"}], + }) + ); + assert_eq!( + search_body["settings"]["allowed_callers"], + json!(["direct"]) + ); + assert_eq!( + search_body["input"] + .as_array() + .context("search input should be an array")? + .last(), + Some(&json!({ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Search the web"}], + })) + ); + + assert_eq!( + requests[1].function_call_output(call_id), + json!({ + "type": "function_call_output", + "call_id": call_id, + "output": [{ + "type": "encrypted_content", + "encrypted_content": "ciphertext", + }], + }) + ); + assert_eq!( + started.item, + ThreadItem::WebSearch { + id: call_id.to_string(), + query: String::new(), + action: Some(WebSearchAction::Other), + } + ); + let expected_completed_item = ThreadItem::WebSearch { + id: call_id.to_string(), + query: "standalone web search".to_string(), + action: Some(WebSearchAction::Search { + query: Some("standalone web search".to_string()), + queries: None, + }), + }; + assert_eq!(completed.item, expected_completed_item); + + drop(mcp); + let mut reloaded_mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, reloaded_mcp.initialize()).await??; + let read_req = reloaded_mcp + .send_thread_read_request(ThreadReadParams { + thread_id, + include_turns: true, + }) + .await?; + let read_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + reloaded_mcp.read_stream_until_response_message(RequestId::Integer(read_req)), + ) + .await??; + let ThreadReadResponse { thread, .. } = to_response::(read_resp)?; + let persisted_web_searches: Vec<&ThreadItem> = thread + .turns + .iter() + .flat_map(|turn| &turn.items) + .filter(|item| matches!(item, ThreadItem::WebSearch { .. })) + .collect(); + assert_eq!(persisted_web_searches, vec![&expected_completed_item]); + + Ok(()) +} + +async fn wait_for_web_search_started(mcp: &mut TestAppServer) -> Result { + loop { + let notification = mcp + .read_stream_until_notification_message("item/started") + .await?; + let started: ItemStartedNotification = serde_json::from_value( + notification + .params + .context("item/started notification should include params")?, + )?; + if matches!(&started.item, ThreadItem::WebSearch { .. }) { + return Ok(started); + } + } +} + +async fn wait_for_web_search_completed( + mcp: &mut TestAppServer, +) -> Result { + loop { + let notification = mcp + .read_stream_until_notification_message("item/completed") + .await?; + let completed: ItemCompletedNotification = serde_json::from_value( + notification + .params + .context("item/completed notification should include params")?, + )?; + if matches!(&completed.item, ThreadItem::WebSearch { .. }) { + return Ok(completed); + } + } +} + +async fn mount_search_response(server: &MockServer) { + Mock::given(method("POST")) + .and(path("/api/codex/alpha/search")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "encrypted_output": "ciphertext", + }))) + .expect(1) + .mount(server) + .await; +} + +fn has_hosted_web_search(body: &Value) -> bool { + body.get("tools") + .and_then(Value::as_array) + .is_some_and(|tools| { + tools + .iter() + .any(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search")) + }) +} + +async fn search_request_body(server: &MockServer) -> Result { + server + .received_requests() + .await + .context("failed to fetch received requests")? + .into_iter() + .find(|request| request.url.path() == "/api/codex/alpha/search") + .context("expected standalone search request")? + .body_json() + .context("search request body should be JSON") +} + +fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { + std::fs::write( + codex_home.join("config.toml"), + format!( + r#" +model = "mock-model" +approval_policy = "never" +sandbox_mode = "read-only" +model_provider = "openai-custom" +chatgpt_base_url = "{server_uri}" + +[features] +standalone_web_search = true + +[model_providers.openai-custom] +name = "OpenAI" +base_url = "{server_uri}/api/codex" +wire_api = "responses" +request_max_retries = 0 +stream_max_retries = 0 +supports_websockets = false +requires_openai_auth = true +"# + ), + ) +} diff --git a/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs index a0466a459b..afb2b2ab33 100644 --- a/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs +++ b/codex-rs/app-server/tests/suite/v2/windows_sandbox_setup.rs @@ -1,6 +1,6 @@ use anyhow::Context; use anyhow::Result; -use app_test_support::McpProcess; +use app_test_support::TestAppServer; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml; @@ -31,7 +31,7 @@ async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<( "mock_provider", "compact prompt", )?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp @@ -66,7 +66,7 @@ async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<( #[tokio::test] async fn windows_sandbox_setup_start_rejects_relative_cwd() -> Result<()> { let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; + let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let request_id = mcp diff --git a/codex-rs/apply-patch/BUILD.bazel b/codex-rs/apply-patch/BUILD.bazel index e68984bc37..b43e8ed6aa 100644 --- a/codex-rs/apply-patch/BUILD.bazel +++ b/codex-rs/apply-patch/BUILD.bazel @@ -1,11 +1,6 @@ load("//:defs.bzl", "codex_rust_crate") -exports_files(["apply_patch_tool_instructions.md"]) - codex_rust_crate( name = "apply-patch", crate_name = "codex_apply_patch", - compile_data = [ - "apply_patch_tool_instructions.md", - ], ) diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index aa2326ab28..29d42e1c07 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -31,9 +31,6 @@ pub use standalone_executable::main; use crate::invocation::ExtractHeredocError; -/// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool. -pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md"); - /// Special argv[1] flag used when the Codex executable self-invokes to run the /// internal `apply_patch` path. /// @@ -776,7 +773,7 @@ fn compute_replacements( } } - replacements.sort_by(|(lhs_idx, _, _), (rhs_idx, _, _)| lhs_idx.cmp(rhs_idx)); + replacements.sort_by_key(|(index, _, _)| *index); Ok(replacements) } diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 43d6d14643..786ef3a9d3 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::ConfigBundleResponse; use crate::types::ConfigFileResponse; use crate::types::ListCredentialRoutesResponse; use crate::types::PaginatedListTaskListItem; @@ -423,6 +424,23 @@ impl Client { } } + /// Fetch the selected cloud-managed config bundle from codex-backend. + /// + /// `GET /api/codex/config/bundle` (Codex API style) or + /// `GET /wham/config/bundle` (ChatGPT backend-api style). + pub async fn get_config_bundle( + &self, + ) -> std::result::Result { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/config/bundle", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/config/bundle", self.base_url), + }; + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?; + self.decode_json::(&url, &ct, &body) + .map_err(RequestError::from) + } + /// 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 4e85b992d7..28a417a308 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -6,8 +6,12 @@ pub use client::Client; pub use client::RequestError; pub use types::CodeTaskDetailsResponse; pub use types::CodeTaskDetailsResponseExt; +pub use types::ConfigBundleResponse; pub use types::ConfigFileResponse; pub use types::CredentialRouteAuthType; +pub use types::DeliveredConfigToml; +pub use types::DeliveredRequirementsToml; +pub use types::DeliveredTomlFragment; pub use types::ListCredentialRoutesResponse; pub use types::PaginatedListTaskListItem; pub use types::ResolvedCredentialRoute; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 6ad6859859..2cac1ad688 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -1,5 +1,9 @@ +pub use codex_backend_openapi_models::models::ConfigBundleResponse; pub use codex_backend_openapi_models::models::ConfigFileResponse; pub use codex_backend_openapi_models::models::CreditStatusDetails; +pub use codex_backend_openapi_models::models::DeliveredConfigToml; +pub use codex_backend_openapi_models::models::DeliveredRequirementsToml; +pub use codex_backend_openapi_models::models::DeliveredTomlFragment; pub use codex_backend_openapi_models::models::PaginatedListTaskListItem; pub use codex_backend_openapi_models::models::PlanType; pub use codex_backend_openapi_models::models::RateLimitReachedKind; diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 2e54192e85..63b34e7355 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::collections::HashSet; use std::time::Duration; @@ -154,20 +155,21 @@ pub fn connectors_for_plugin_apps( connectors: Vec, plugin_apps: &[AppConnectorId], ) -> Vec { - let plugin_app_ids = plugin_apps - .iter() - .map(|connector_id| connector_id.0.as_str()) - .collect::>(); - let connectors = merge_plugin_connectors( connectors, plugin_apps .iter() .map(|connector_id| connector_id.0.clone()), ); - filter_disallowed_connectors(connectors, originator().value.as_str()) - .into_iter() - .filter(|connector| plugin_app_ids.contains(connector.id.as_str())) + let mut connectors_by_id = + filter_disallowed_connectors(connectors, originator().value.as_str()) + .into_iter() + .map(|connector| (connector.id.clone(), connector)) + .collect::>(); + + plugin_apps + .iter() + .filter_map(|connector_id| connectors_by_id.remove(connector_id.0.as_str())) .collect() } @@ -266,13 +268,14 @@ mod tests { let connectors = connectors_for_plugin_apps( vec![app("alpha"), app("beta")], &[ + AppConnectorId("gmail".to_string()), AppConnectorId("alpha".to_string()), AppConnectorId("gmail".to_string()), ], ); assert_eq!( connectors, - vec![app("alpha"), merged_app("gmail", /*is_accessible*/ false)] + vec![merged_app("gmail", /*is_accessible*/ false), app("alpha")] ); } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 1a1b852667..6a4d78fbc7 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -83,6 +83,7 @@ tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } unicode-segmentation = { workspace = true } +url = { workspace = true } which = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 8aa6734837..f0058851d3 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -23,6 +23,7 @@ use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams; #[cfg(target_os = "macos")] use codex_sandboxing::seatbelt::create_seatbelt_command_args; +use codex_sandboxing::with_managed_mitm_ca_readable_root; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; use tokio::process::Child; @@ -208,10 +209,12 @@ async fn run_command_under_sandbox( // In practice, this should be `std::env::current_dir()` because this CLI // does not support `--cwd`, but let's use the config value for consistency. let cwd = config.cwd.clone(); - // For now, we always use the same cwd for both the command and the - // sandbox policy. In the future, we could add a CLI option to set them - // separately. + // Non-Windows sandbox launchers still use `sandbox_policy_cwd` for any + // remaining cwd-dependent policy resolution. `:workspace_roots` entries in + // the effective profile have already been materialized from config roots. let sandbox_policy_cwd = cwd.clone(); + #[cfg(target_os = "windows")] + let workspace_roots = config.effective_workspace_roots(); let env = create_env( &config.permissions.shell_environment_policy, @@ -222,7 +225,7 @@ async fn run_command_under_sandbox( if let SandboxType::Windows = sandbox_type { #[cfg(target_os = "windows")] { - run_command_under_windows_session(&config, command, cwd, sandbox_policy_cwd, env).await; + run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await; } #[cfg(not(target_os = "windows"))] { @@ -255,12 +258,21 @@ async fn run_command_under_sandbox( let network = network_proxy .as_ref() .map(codex_core::config::StartedNetworkProxy::proxy); + let managed_mitm_ca_trust_bundle_path = match network.as_ref() { + Some(network) => network.managed_mitm_ca_trust_bundle_path(), + None => None, + }; + let runtime_permission_profile = with_managed_mitm_ca_readable_root( + config.permissions.effective_permission_profile(), + managed_mitm_ca_trust_bundle_path.as_ref(), + sandbox_policy_cwd.as_path(), + ); let mut child = match sandbox_type { #[cfg(target_os = "macos")] SandboxType::Seatbelt => { - let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy(); - let network_sandbox_policy = config.permissions.network_sandbox_policy(); + let (file_system_sandbox_policy, network_sandbox_policy) = + runtime_permission_profile.to_runtime_permissions(); let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command, file_system_sandbox_policy: &file_system_sandbox_policy, @@ -292,11 +304,11 @@ async fn run_command_under_sandbox( .codex_linux_sandbox_exe .expect("codex-linux-sandbox executable not found"); let use_legacy_landlock = config.features.use_legacy_landlock(); - let network_sandbox_policy = config.permissions.network_sandbox_policy(); + let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy(); let args = create_linux_sandbox_command_args_for_permission_profile( command, cwd.as_path(), - &config.permissions.effective_permission_profile(), + &runtime_permission_profile, sandbox_policy_cwd.as_path(), use_legacy_landlock, allow_network_for_proxy(managed_network_requirements_enabled), @@ -350,24 +362,15 @@ async fn run_command_under_windows_session( config: &Config, command: Vec, cwd: AbsolutePathBuf, - sandbox_policy_cwd: AbsolutePathBuf, + workspace_roots: Vec, env: std::collections::HashMap, ) -> ! { use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::WindowsSandboxLevel; - use codex_windows_sandbox::spawn_windows_sandbox_session_elevated; + use codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile; use codex_windows_sandbox::spawn_windows_sandbox_session_legacy; - let sandbox_policy = config - .permissions - .legacy_sandbox_policy(sandbox_policy_cwd.as_path()); - let policy_str = match serde_json::to_string(&sandbox_policy) { - Ok(policy_str) => policy_str, - Err(err) => { - eprintln!("windows sandbox failed to serialize policy: {err}"); - std::process::exit(1); - } - }; + let permission_profile = config.permissions.effective_permission_profile(); let use_elevated = matches!( WindowsSandboxLevel::from_config(config), @@ -375,9 +378,9 @@ async fn run_command_under_windows_session( ); let spawned = if use_elevated { - spawn_windows_sandbox_session_elevated( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + spawn_windows_sandbox_session_elevated_for_permission_profile( + &permission_profile, + workspace_roots.as_slice(), config.codex_home.as_path(), command, cwd.as_path(), @@ -395,8 +398,8 @@ async fn run_command_under_windows_session( .await } else { spawn_windows_sandbox_session_legacy( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), config.codex_home.as_path(), command, cwd.as_path(), @@ -1134,7 +1137,7 @@ mod tests { } #[tokio::test] - async fn debug_sandbox_uses_explicit_profile_cwd() -> anyhow::Result<()> { + async fn debug_sandbox_uses_explicit_cwd() -> anyhow::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs index 10a47806b1..85928b551f 100644 --- a/codex-rs/cli/src/desktop_app/mac.rs +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -82,10 +82,11 @@ async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> "Opening workspace {workspace}...", workspace = workspace.display() ); + let url = codex_new_thread_url(workspace); let status = Command::new("open") .arg("-a") .arg(app_path) - .arg(workspace) + .arg(&url) .status() .await .context("failed to invoke `open`")?; @@ -95,12 +96,20 @@ async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> } anyhow::bail!( - "`open -a {app_path} {workspace}` exited with {status}", + "`open -a {app_path} {url}` exited with {status}", app_path = app_path.display(), - workspace = workspace.display() + url = url ); } +fn codex_new_thread_url(workspace: &Path) -> String { + let workspace = workspace.as_os_str().to_string_lossy(); + let mut serializer = url::form_urlencoded::Serializer::new(String::new()); + serializer.append_pair("path", workspace.as_ref()); + let query = serializer.finish(); + format!("codex://threads/new?{query}") +} + async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyhow::Result { let temp_dir = Builder::new() .prefix("codex-app-installer-") @@ -293,8 +302,10 @@ fn parse_hdiutil_attach_mount_point(output: &str) -> Option { #[cfg(test)] mod tests { + use super::codex_new_thread_url; use super::parse_hdiutil_attach_mount_point; use pretty_assertions::assert_eq; + use std::path::Path; #[test] fn parses_mount_point_from_tab_separated_hdiutil_output() { @@ -313,4 +324,25 @@ mod tests { Some("/Volumes/Codex Installer") ); } + + #[test] + fn codex_new_thread_url_encodes_workspace_path() { + let url = url::Url::parse(&codex_new_thread_url(Path::new("/tmp/codex workspace/#1"))) + .expect("deep link should parse"); + + assert_eq!( + ( + url.scheme().to_string(), + url.host_str().map(str::to_string), + url.path().to_string(), + url.query_pairs().into_owned().collect::>(), + ), + ( + "codex".to_string(), + Some("threads".to_string()), + "/new".to_string(), + vec![("path".to_string(), "/tmp/codex workspace/#1".to_string())], + ) + ); + } } diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index d2422d05de..2e53d5d51f 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2176,11 +2176,7 @@ struct RolloutStats { impl RolloutStats { fn average_bytes(&self) -> u64 { - if self.files == 0 { - 0 - } else { - self.total_bytes / self.files - } + self.total_bytes.checked_div(self.files).unwrap_or(0) } } diff --git a/codex-rs/cli/src/doctor/title.rs b/codex-rs/cli/src/doctor/title.rs index 0f9c803ddf..09dc80c0a2 100644 --- a/codex-rs/cli/src/doctor/title.rs +++ b/codex-rs/cli/src/doctor/title.rs @@ -143,6 +143,7 @@ fn terminal_title_item_id(item: &str) -> Option<&'static str> { "fast-mode" => Some("fast-mode"), "model" | "model-name" => Some("model"), "model-with-reasoning" => Some("model-with-reasoning"), + "reasoning" => Some("reasoning"), "task-progress" => Some("task-progress"), _ => None, } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 856273354c..2a5ce171b7 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -35,6 +35,7 @@ use codex_tui::UpdateAction; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_cli::CliConfigOverrides; use codex_utils_cli::ProfileV2Name; +use codex_utils_cli::SharedCliOptions; use codex_utils_cli::resume_hint; use owo_colors::OwoColorize; use std::io::IsTerminal; @@ -50,6 +51,8 @@ mod marketplace_cmd; mod mcp_cmd; mod plugin_cmd; mod remote_control_cmd; +#[cfg(target_os = "windows")] +mod sandbox_setup; mod state_db_recovery; #[cfg(not(windows))] mod wsl_paths; @@ -173,6 +176,12 @@ enum Subcommand { /// Resume a previous interactive session (picker by default; use --last to continue the most recent). Resume(ResumeCommand), + /// Archive a saved session by id or session name. + Archive(SessionArchiveCommand), + + /// Unarchive a saved session by id or session name. + Unarchive(SessionArchiveCommand), + /// Fork a previous interactive session (picker by default; use --last to fork the most recent). Fork(ForkCommand), @@ -294,7 +303,7 @@ struct DebugTraceReduceCommand { #[derive(Debug, Parser)] struct ResumeCommand { - /// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses. + /// Session id (UUID) or session name. UUIDs take precedence if it parses. /// If omitted, use --last to pick the most recent recorded session. #[arg(value_name = "SESSION_ID")] session_id: Option, @@ -318,6 +327,32 @@ struct ResumeCommand { config_overrides: TuiCli, } +#[derive(Debug, Parser)] +struct SessionArchiveCommand { + /// Session id (UUID) or session name. UUIDs take precedence if it parses. + #[arg(value_name = "SESSION")] + target: String, + + #[clap(flatten)] + remote: InteractiveRemoteOptions, + + #[clap(flatten)] + config_overrides: SessionArchiveConfigOverrides, +} + +#[derive(Debug, Args, Clone, Default)] +struct SessionArchiveConfigOverrides { + #[clap(flatten)] + shared: SharedCliOptions, + + /// Error out when config.toml contains fields that are not recognized by this version of Codex. + #[arg(long = "strict-config", default_value_t = false)] + strict_config: bool, + + #[clap(flatten)] + config_overrides: CliConfigOverrides, +} + #[derive(Debug, Parser)] struct ForkCommand { /// Conversation/session id (UUID). When provided, forks this session. @@ -452,6 +487,10 @@ struct AppServerCommand { )] listen: codex_app_server::AppServerTransport, + /// Use stdio as the transport (equivalent to `--listen stdio://`). + #[arg(long = "stdio", conflicts_with = "listen")] + stdio: bool, + /// Enable remote control for this app-server process. #[arg(long = "remote-control", hide = true)] remote_control: bool, @@ -730,6 +769,39 @@ fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> { cmd.run() } +async fn run_session_archive_cli_command( + action: codex_tui::SessionArchiveAction, + cmd: SessionArchiveCommand, + mut interactive: TuiCli, + root_config_overrides: CliConfigOverrides, + root_remote: Option, + root_remote_auth_token_env: Option, + arg0_paths: Arg0DispatchPaths, +) -> anyhow::Result { + let SessionArchiveCommand { + target, + remote, + config_overrides, + } = cmd; + interactive = + finalize_session_archive_interactive(interactive, root_config_overrides, config_overrides); + let explicit_remote_endpoint = resolve_remote_endpoint( + remote.remote.or(root_remote), + remote.remote_auth_token_env.or(root_remote_auth_token_env), + )?; + codex_tui::run_session_archive_command( + action, + target, + codex_tui::SessionArchiveCommandOptions { + cli: interactive, + arg0_paths, + explicit_remote_endpoint, + }, + ) + .await + .map_err(|err| anyhow::anyhow!("{err}")) +} + async fn run_debug_app_server_command(cmd: DebugAppServerCommand) -> anyhow::Result<()> { match cmd.subcommand { DebugAppServerSubcommand::SendMessageV2(cmd) => { @@ -965,6 +1037,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { subcommand, strict_config: app_server_strict_config, listen, + stdio, remote_control, analytics_default_enabled, auth, @@ -978,7 +1051,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { )?; match subcommand { None => { - let transport = listen; + let transport = if stdio { + codex_app_server::AppServerTransport::Stdio + } else { + listen + }; let auth = auth.try_into_settings()?; let runtime_options = codex_app_server::AppServerRuntimeOptions { remote_control_enabled: remote_control, @@ -1115,6 +1192,32 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { .await?; handle_app_exit(exit_info)?; } + Some(Subcommand::Archive(cmd)) => { + let output = run_session_archive_cli_command( + codex_tui::SessionArchiveAction::Archive, + cmd, + interactive, + root_config_overrides.clone(), + root_remote.clone(), + root_remote_auth_token_env.clone(), + arg0_paths.clone(), + ) + .await?; + println!("{output}"); + } + Some(Subcommand::Unarchive(cmd)) => { + let output = run_session_archive_cli_command( + codex_tui::SessionArchiveAction::Unarchive, + cmd, + interactive, + root_config_overrides.clone(), + root_remote.clone(), + root_remote_auth_token_env.clone(), + arg0_paths.clone(), + ) + .await?; + println!("{output}"); + } Some(Subcommand::Fork(ForkCommand { session_id, last, @@ -1241,6 +1344,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { .await?; } Some(Subcommand::Sandbox(mut sandbox_cli)) => { + #[cfg(target_os = "windows")] + if let Some(setup_cli) = sandbox_setup::parse_setup_command(&sandbox_cli.command)? { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "sandbox setup", + )?; + sandbox_setup::run(setup_cli).await?; + return Ok(()); + } reject_remote_mode_for_subcommand( root_remote.as_deref(), root_remote_auth_token_env.as_deref(), @@ -1454,6 +1567,8 @@ fn profile_v2_for_subcommand<'a>( Subcommand::Exec(_) | Subcommand::Review(_) | Subcommand::Resume(_) + | Subcommand::Archive(_) + | Subcommand::Unarchive(_) | Subcommand::Fork(_) | Subcommand::Mcp(_) | Subcommand::Sandbox(_) @@ -1461,7 +1576,7 @@ fn profile_v2_for_subcommand<'a>( subcommand: DebugSubcommand::PromptInput(_), }) => Ok(Some(profile_v2)), _ => anyhow::bail!( - "--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`." + "--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex archive`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`." ), } } @@ -1486,7 +1601,8 @@ async fn run_exec_server_command( .ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?; let config = load_exec_server_config(root_config_overrides, strict_config).await?; let auth_provider = - load_exec_server_remote_auth_provider(&config, cmd.use_agent_identity_auth).await?; + load_exec_server_remote_auth_provider(&config, &base_url, cmd.use_agent_identity_auth) + .await?; let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new( base_url, environment_id, @@ -1516,6 +1632,7 @@ async fn run_exec_server_command( async fn load_exec_server_remote_auth_provider( config: &codex_core::config::Config, + base_url: &str, use_agent_identity_auth: bool, ) -> anyhow::Result { if use_agent_identity_auth { @@ -1530,19 +1647,61 @@ async fn load_exec_server_remote_auth_provider( let auth = load_exec_server_remote_auth( config, - "remote exec-server registration requires ChatGPT authentication; run `codex login` first", + "remote exec-server registration requires ChatGPT authentication or API key authentication; run `codex login` or set CODEX_API_KEY", ) .await?; - if !auth.is_chatgpt_auth() { + if !is_supported_exec_server_remote_auth(&auth) { anyhow::bail!( - "remote exec-server registration requires ChatGPT authentication; API key and Agent Identity auth are not supported" + "remote exec-server registration requires ChatGPT authentication or API key authentication; Agent Identity auth requires --use-agent-identity-auth" ); } + if auth.is_api_key_auth() { + validate_api_key_remote_host(base_url)?; + } + Ok(codex_model_provider::auth_provider_from_auth(&auth)) } +fn is_supported_exec_server_remote_auth(auth: &CodexAuth) -> bool { + auth.is_chatgpt_auth() || auth.is_api_key_auth() +} + +fn validate_api_key_remote_host(base_url: &str) -> anyhow::Result<()> { + let url = url::Url::parse(base_url) + .map_err(|err| anyhow::anyhow!("invalid remote exec-server registration URL: {err}"))?; + let host = url.host().ok_or_else(|| { + anyhow::anyhow!("remote exec-server registration URL must include a host") + })?; + + let is_loopback = match &host { + url::Host::Domain(host) => host.eq_ignore_ascii_case("localhost"), + url::Host::Ipv4(ip) => ip.is_loopback(), + url::Host::Ipv6(ip) => ip.is_loopback(), + }; + let is_openai_host = match &host { + url::Host::Domain(host) => ["openai.com", "openai.org"].into_iter().any(|domain| { + host.eq_ignore_ascii_case(domain) + || host.to_ascii_lowercase().ends_with(&format!(".{domain}")) + }), + _ => false, + }; + let is_allowed = match url.scheme() { + "https" => is_loopback || is_openai_host, + "http" => is_loopback, + _ => false, + }; + + if !is_allowed { + anyhow::bail!( + "remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts" + ); + } + + Ok(()) +} + async fn load_exec_server_config( root_config_overrides: &CliConfigOverrides, strict_config: bool, @@ -1837,6 +1996,8 @@ fn unsupported_subcommand_name_for_strict_config( | Some(Subcommand::McpServer(_)) | Some(Subcommand::ExecServer(_)) | Some(Subcommand::Resume(_)) + | Some(Subcommand::Archive(_)) + | Some(Subcommand::Unarchive(_)) | Some(Subcommand::Fork(_)) | Some(Subcommand::Doctor(_)) => None, Some(Subcommand::AppServer(app_server)) if app_server.subcommand.is_none() => None, @@ -1984,34 +2145,13 @@ async fn run_interactive_tui( } } - let mut remote_endpoint = remote - .as_deref() - .map(codex_tui::resolve_remote_addr) - .transpose() - .map_err(std::io::Error::other)?; - if let Some(remote_auth_token_env) = remote_auth_token_env { - let Some(endpoint) = remote_endpoint.as_mut() else { - return Ok(AppExitInfo::fatal( - "`--remote-auth-token-env` requires `--remote`.", - )); - }; - if !codex_tui::remote_addr_supports_auth_token(endpoint) { - return Ok(AppExitInfo::fatal( - "`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.", - )); + let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env) { + Ok(remote_endpoint) => remote_endpoint, + Err(err) if is_remote_auth_usage_error(&err) => { + return Ok(AppExitInfo::fatal(err.to_string())); } - let auth_token = read_remote_auth_token_from_env_var(&remote_auth_token_env) - .map_err(std::io::Error::other)?; - let codex_tui::RemoteAppServerEndpoint::WebSocket { - auth_token: slot, .. - } = endpoint - else { - return Ok(AppExitInfo::fatal( - "`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.", - )); - }; - *slot = Some(auth_token); - } + Err(err) => return Err(err), + }; let start_tui = || { codex_tui::run_main( interactive.clone(), @@ -2055,6 +2195,46 @@ async fn run_interactive_tui( } } +fn resolve_remote_endpoint( + remote: Option, + remote_auth_token_env: Option, +) -> std::io::Result> { + let mut remote_endpoint = remote + .as_deref() + .map(codex_tui::resolve_remote_addr) + .transpose() + .map_err(std::io::Error::other)?; + if let Some(remote_auth_token_env) = remote_auth_token_env { + let Some(endpoint) = remote_endpoint.as_mut() else { + return Err(std::io::Error::other( + "`--remote-auth-token-env` requires `--remote`.", + )); + }; + if !codex_tui::remote_addr_supports_auth_token(endpoint) { + return Err(std::io::Error::other( + "`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.", + )); + } + let auth_token = read_remote_auth_token_from_env_var(&remote_auth_token_env) + .map_err(std::io::Error::other)?; + let codex_tui::RemoteAppServerEndpoint::WebSocket { + auth_token: slot, .. + } = endpoint + else { + return Err(std::io::Error::other( + "`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.", + )); + }; + *slot = Some(auth_token); + } + Ok(remote_endpoint) +} + +fn is_remote_auth_usage_error(err: &std::io::Error) -> bool { + err.to_string() + .starts_with("`--remote-auth-token-env` requires") +} + fn confirm(prompt: &str) -> std::io::Result { eprintln!("{prompt}"); @@ -2118,9 +2298,31 @@ fn finalize_fork_interactive( interactive } -/// Merge flags provided to `codex resume`/`codex fork` so they take precedence over any -/// root-level flags. Only overrides fields explicitly set on the subcommand-scoped -/// CLI. Also appends `-c key=value` overrides with highest precedence. +fn finalize_session_archive_interactive( + mut interactive: TuiCli, + root_config_overrides: CliConfigOverrides, + archive_cli: SessionArchiveConfigOverrides, +) -> TuiCli { + let SessionArchiveConfigOverrides { + shared, + strict_config, + config_overrides, + } = archive_cli; + interactive.shared.apply_subcommand_overrides(shared); + if strict_config { + interactive.strict_config = true; + } + interactive + .config_overrides + .raw_overrides + .extend(config_overrides.raw_overrides); + prepend_config_flags(&mut interactive.config_overrides, root_config_overrides); + interactive +} + +/// Merge flags provided to runtime wrapper commands so they take precedence over any root-level +/// flags. Only overrides fields explicitly set on the subcommand-scoped CLI. Also appends +/// `-c key=value` overrides with highest precedence. fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) { let TuiCli { shared, @@ -2168,6 +2370,63 @@ mod tests { use codex_tui::TokenUsage; use pretty_assertions::assert_eq; + #[test] + fn exec_server_remote_auth_accepts_api_key_auth() { + let auth = CodexAuth::from_api_key("sk-test"); + + assert!(is_supported_exec_server_remote_auth(&auth)); + } + + #[test] + fn exec_server_remote_api_key_auth_accepts_https_openai_domains() { + for base_url in [ + "https://openai.com/api", + "https://service.openai.com/api", + "https://openai.org/api", + "https://service.openai.org/api", + ] { + assert!(validate_api_key_remote_host(base_url).is_ok()); + } + } + + #[test] + fn exec_server_remote_api_key_auth_accepts_http_loopback() { + for base_url in [ + "http://localhost:8098/api", + "http://127.0.0.1:8098/api", + "http://[::1]:8098/api", + ] { + assert!(validate_api_key_remote_host(base_url).is_ok()); + } + } + + #[test] + fn exec_server_remote_api_key_auth_rejects_http_openai_domain() { + for base_url in [ + "http://service.openai.com/api", + "http://service.openai.org/api", + ] { + let error = validate_api_key_remote_host(base_url) + .expect_err("reject plaintext OpenAI destination"); + + assert_eq!( + error.to_string(), + "remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts" + ); + } + } + + #[test] + fn exec_server_remote_api_key_auth_rejects_suffix_spoof() { + let error = validate_api_key_remote_host("https://service.openai.org.evil.example/api") + .expect_err("reject suffix spoof"); + + assert_eq!( + error.to_string(), + "remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts" + ); + } + fn finalize_resume_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let MultitoolCli { @@ -2225,6 +2484,32 @@ mod tests { finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli) } + fn finalize_archive_from_args(args: &[&str]) -> (String, TuiCli, InteractiveRemoteOptions) { + let cli = MultitoolCli::try_parse_from(args).expect("parse"); + let MultitoolCli { + interactive, + config_overrides: root_overrides, + subcommand, + feature_toggles: _, + remote: _, + } = cli; + + let Subcommand::Archive(SessionArchiveCommand { + target, + remote, + config_overrides: archive_cli, + }) = subcommand.expect("archive present") + else { + unreachable!() + }; + + ( + target, + finalize_session_archive_interactive(interactive, root_overrides, archive_cli), + remote, + ) + } + fn profile_v2_for_args(args: &[&str]) -> anyhow::Result> { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let Some(subcommand) = cli.subcommand.as_ref() else { @@ -2495,6 +2780,41 @@ mod tests { assert!(matches!(cli.subcommand, Some(Subcommand::Update))); } + #[test] + fn archive_merges_scoped_tui_flags() { + let (target, interactive, remote) = finalize_archive_from_args( + [ + "codex", + "-C", + "/root", + "archive", + "--remote", + "unix://archive.sock", + "--strict-config", + "--dangerously-bypass-hook-trust", + "-m", + "gpt-5.1-test", + "-p", + "work", + "-C", + "/archive", + "my-thread", + ] + .as_ref(), + ); + + assert_eq!(target, "my-thread"); + assert_eq!(remote.remote.as_deref(), Some("unix://archive.sock")); + assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test")); + assert_eq!(interactive.config_profile_v2.as_deref(), Some("work")); + assert_eq!( + interactive.cwd.as_deref(), + Some(std::path::Path::new("/archive")) + ); + assert!(interactive.strict_config); + assert!(interactive.bypass_hook_trust); + } + #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] #[test] fn sandbox_parses_permissions_profile() { @@ -3096,6 +3416,25 @@ mod tests { ); } + #[test] + fn app_server_stdio_flag_parses() { + let app_server = app_server_from_args(["codex", "app-server", "--stdio"].as_ref()); + assert!(app_server.stdio); + } + + #[test] + fn app_server_stdio_flag_conflicts_with_listen() { + let err = MultitoolCli::try_parse_from([ + "codex", + "app-server", + "--stdio", + "--listen", + "stdio://", + ]) + .expect_err("--stdio and --listen should be rejected together"); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + #[test] fn app_server_listen_unix_socket_url_parses() { let app_server = diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index a84d1c9019..0103782653 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -537,7 +537,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> let effective_mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let mut entries: Vec<_> = mcp_servers.iter().collect(); - entries.sort_by(|(a, _), (b, _)| a.cmp(b)); + entries.sort_by_key(|(name, _)| *name); let auth_statuses = compute_auth_statuses( effective_mcp_servers.iter(), config.mcp_oauth_credentials_store_mode, @@ -906,7 +906,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re let headers_display = match http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, _)| format!("{k}=*****")) @@ -919,7 +919,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re let env_headers_display = match env_http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, var)| format!("{k}={var}")) diff --git a/codex-rs/cli/src/sandbox_setup.rs b/codex-rs/cli/src/sandbox_setup.rs new file mode 100644 index 0000000000..958411c428 --- /dev/null +++ b/codex-rs/cli/src/sandbox_setup.rs @@ -0,0 +1,206 @@ +use std::path::PathBuf; + +use clap::ArgAction; +use clap::ArgGroup; +use clap::Parser; +use codex_core::config::edit::ConfigEditsBuilder; +use codex_core::config::find_codex_home; + +#[derive(Debug, Parser)] +#[command(group( + ArgGroup::new("sandbox_user") + .required(true) + .args(["user", "current_user"]) +))] +pub(crate) struct SandboxSetupCommand { + /// Set up the elevated Windows sandbox. + #[arg(long = "elevated", action = ArgAction::SetTrue)] + elevated_sandbox_level: bool, + + /// Windows user that will run Codex after managed deployment. + #[arg( + long = "user", + value_name = "USER", + conflicts_with = "current_user", + requires = "codex_home" + )] + user: Option, + + /// Use the current Windows user as the Codex user. + #[arg( + long = "current-user", + default_value_t = false, + conflicts_with = "user" + )] + current_user: bool, + + /// CODEX_HOME for the Codex user. Required with --user. + #[arg(long = "codex-home", value_name = "DIR")] + codex_home: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SandboxSetupLevel { + Elevated, +} + +impl SandboxSetupCommand { + fn setup_level(&self) -> anyhow::Result { + if self.elevated_sandbox_level { + Ok(SandboxSetupLevel::Elevated) + } else { + anyhow::bail!("`codex sandbox setup` currently requires --elevated"); + } + } +} + +pub(crate) async fn run(cmd: SandboxSetupCommand) -> anyhow::Result<()> { + match cmd.setup_level()? { + SandboxSetupLevel::Elevated => run_elevated(cmd).await, + } +} + +pub(crate) fn parse_setup_command( + sandbox_command: &[String], +) -> anyhow::Result> { + if sandbox_command + .first() + .is_none_or(|command| command != "setup") + { + return Ok(None); + } + + SandboxSetupCommand::try_parse_from(sandbox_command.iter().map(String::as_str)) + .map(Some) + .map_err(anyhow::Error::from) +} + +async fn run_elevated(cmd: SandboxSetupCommand) -> anyhow::Result<()> { + let identity = resolve_sandbox_setup_identity(&cmd)?; + + codex_core::windows_sandbox::run_elevated_provisioning_setup( + identity.codex_home.as_path(), + identity.real_user.as_str(), + )?; + ConfigEditsBuilder::new(identity.codex_home.as_path()) + .set_windows_sandbox_mode("elevated") + .apply() + .await + .map_err(|err| { + anyhow::anyhow!( + "sandbox provisioning succeeded, but failed to persist elevated sandbox config: {err}" + ) + })?; + + println!( + "Windows elevated sandbox setup completed for {} at {}.", + identity.real_user, + identity.codex_home.display() + ); + Ok(()) +} + +struct SandboxSetupIdentity { + real_user: String, + codex_home: PathBuf, +} + +fn resolve_sandbox_setup_identity( + cmd: &SandboxSetupCommand, +) -> anyhow::Result { + if cmd.current_user { + let real_user = std::env::var("USERNAME") + .or_else(|_| std::env::var("USER")) + .map_err(|err| { + anyhow::anyhow!("failed to determine current user from environment: {err}") + })?; + let codex_home = match cmd.codex_home.clone() { + Some(codex_home) => codex_home, + None => find_codex_home()?.to_path_buf(), + }; + return Ok(SandboxSetupIdentity { + real_user, + codex_home, + }); + } + + let real_user = cmd + .user + .clone() + .ok_or_else(|| anyhow::anyhow!("--user or --current-user is required"))?; + let codex_home = cmd + .codex_home + .clone() + .ok_or_else(|| anyhow::anyhow!("--codex-home is required with --user"))?; + Ok(SandboxSetupIdentity { + real_user, + codex_home, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_managed_user_identity() { + let command = SandboxSetupCommand::try_parse_from([ + "setup", + "--elevated", + "--user", + "DOMAIN\\alice", + "--codex-home", + r"C:\Users\alice\.codex", + ]) + .expect("parse"); + + assert!(command.elevated_sandbox_level); + assert_eq!(command.user.as_deref(), Some(r"DOMAIN\alice")); + assert!(!command.current_user); + assert_eq!( + command.codex_home.as_deref(), + Some(std::path::Path::new(r"C:\Users\alice\.codex")) + ); + } + + #[test] + fn requires_explicit_user_identity() { + let err = SandboxSetupCommand::try_parse_from(["setup", "--elevated"]) + .expect_err("parse should fail"); + + assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[test] + fn requires_codex_home_for_managed_user() { + let err = + SandboxSetupCommand::try_parse_from(["setup", "--elevated", "--user", "DOMAIN\\alice"]) + .expect_err("parse should fail"); + + assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[test] + fn parses_setup_from_sandbox_command_args() { + let command = parse_setup_command(&[ + "setup".to_string(), + "--elevated".to_string(), + "--user".to_string(), + r"DOMAIN\alice".to_string(), + "--codex-home".to_string(), + r"C:\Users\alice\.codex".to_string(), + ]) + .expect("parse") + .expect("setup command"); + + assert_eq!(command.user.as_deref(), Some(r"DOMAIN\alice")); + } + + #[test] + fn ignores_non_setup_sandbox_command_args() { + let command = + parse_setup_command(&["echo".to_string(), "hello".to_string()]).expect("parse"); + + assert!(command.is_none()); + } +} diff --git a/codex-rs/cloud-config/BUILD.bazel b/codex-rs/cloud-config/BUILD.bazel new file mode 100644 index 0000000000..b11ed2ca49 --- /dev/null +++ b/codex-rs/cloud-config/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "cloud-config", + crate_name = "codex_cloud_config", +) diff --git a/codex-rs/cloud-requirements/Cargo.toml b/codex-rs/cloud-config/Cargo.toml similarity index 96% rename from codex-rs/cloud-requirements/Cargo.toml rename to codex-rs/cloud-config/Cargo.toml index f4b2d2dbcb..aeba70a489 100644 --- a/codex-rs/cloud-requirements/Cargo.toml +++ b/codex-rs/cloud-config/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "codex-cloud-requirements" +name = "codex-cloud-config" version.workspace = true edition.workspace = true license.workspace = true diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-config/src/lib.rs similarity index 99% rename from codex-rs/cloud-requirements/src/lib.rs rename to codex-rs/cloud-config/src/lib.rs index 19fbd8cd1d..0beb91780c 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-config/src/lib.rs @@ -1222,6 +1222,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1308,6 +1309,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1345,6 +1347,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1399,6 +1402,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1582,6 +1586,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1666,6 +1671,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1748,6 +1754,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1958,6 +1965,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2002,6 +2010,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2066,6 +2075,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2126,6 +2136,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2188,6 +2199,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2251,6 +2263,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2318,6 +2331,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2411,6 +2425,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2450,6 +2465,7 @@ command = "sample-mcp" allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, diff --git a/codex-rs/cloud-requirements/BUILD.bazel b/codex-rs/cloud-requirements/BUILD.bazel deleted file mode 100644 index 88243aff90..0000000000 --- a/codex-rs/cloud-requirements/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "cloud-requirements", - crate_name = "codex_cloud_requirements", -) diff --git a/codex-rs/code-mode/Cargo.toml b/codex-rs/code-mode/Cargo.toml index 19d6c3ab45..879404d8aa 100644 --- a/codex-rs/code-mode/Cargo.toml +++ b/codex-rs/code-mode/Cargo.toml @@ -16,8 +16,6 @@ sandbox = ["v8/v8_enable_sandbox"] workspace = true [dependencies] -async-channel = { workspace = true } -async-trait = { workspace = true } codex-protocol = { workspace = true } deno_core_icudata = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 18d3c4555c..50fcc034ba 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use crate::PUBLIC_TOOL_NAME; const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1; -const DEFERRED_NESTED_TOOLS_GUIDANCE: &str = r#"Some nested MCP/app tools may be omitted from this description. They are still available on the global `tools` object and listed in `ALL_TOOLS`. +const DEFERRED_NESTED_TOOLS_GUIDANCE: &str = r#"Some deferred nested tools may be omitted from this description. They are still available on the global `tools` object and listed in `ALL_TOOLS`. To find one, filter `ALL_TOOLS` by `name` and `description`."#; const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in a fresh V8 isolate as an async module. @@ -17,14 +17,14 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co - Runs raw JavaScript -- no Node, no file system, no network access, no console. - Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences. - You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. -- `yield_time_ms` asks `exec` to yield early after that many milliseconds if the script is still running. -- `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. +- `yield_time_ms` asks `exec` to yield early if the script is still running. Defaults to 10000 ms. +- `max_output_tokens` sets the token budget for direct `exec` results. Defaults to 10000 tokens. - When the JS code is fully evaluated, the isolate's lifetime ends and unawaited promises are silently discarded. - Global helpers: - `exit()`: Immediately ends the current script successfully (like an early return from the top level). - `text(value: string | number | boolean | undefined | null)`: Appends a text item. Non-string values are stringified with `JSON.stringify(...)` when possible. -- `image(imageUrlOrItem: string | { image_url: string; detail?: "high" | "original" | null } | ImageContent, detail?: "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. +- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. - `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. - `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. - `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`. @@ -34,9 +34,9 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co - `yield_control()`: yields the accumulated output to the model immediately while the script keeps running."#; const WAIT_DESCRIPTION_TEMPLATE: &str = r#"- Use `wait` only after `exec` returns `Script running with cell ID ...`. - `cell_id` identifies the running `exec` cell to resume. -- `yield_time_ms` controls how long to wait for more output before yielding again. If omitted, `wait` uses its default wait timeout. -- `max_tokens` limits how much new output this wait call returns. -- `terminate: true` stops the running cell instead of waiting for more output. +- `yield_time_ms` controls how long to wait for more output before yielding again. Defaults to 10000 ms. +- `max_tokens` limits how much new output this wait call returns. Defaults to 10000 tokens. +- `terminate: true` stops the running cell; false or omitted waits for output. - `wait` returns only the new output since the last yield, or the final completion or termination result for that cell. - If the cell is still running, `wait` may yield again with the same `cell_id`. - If the cell has already finished, `wait` returns the completed result and closes the cell."#; @@ -649,7 +649,7 @@ fn render_json_schema_object(map: &serde_json::Map) -> String .unwrap_or_default(); let mut sorted_properties = properties.iter().collect::>(); - sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); + sorted_properties.sort_unstable_by_key(|(name_a, _)| *name_a); if sorted_properties .iter() .any(|(_, value)| has_property_description(value)) @@ -1091,7 +1091,7 @@ bar" /*deferred_tools_available*/ true, ); - assert!(description.contains("Some nested MCP/app tools may be omitted")); + assert!(description.contains("Some deferred nested tools may be omitted")); assert!(description.contains("filter `ALL_TOOLS` by `name` and `description`")); assert!(!description.contains("do not print the full `ALL_TOOLS` array")); } diff --git a/codex-rs/code-mode/src/lib.rs b/codex-rs/code-mode/src/lib.rs index 3da8c77325..37ba1d2a91 100644 --- a/codex-rs/code-mode/src/lib.rs +++ b/codex-rs/code-mode/src/lib.rs @@ -29,9 +29,18 @@ pub use runtime::WaitOutcome; pub use runtime::WaitRequest; pub use runtime::WaitToPendingOutcome; pub use runtime::WaitToPendingRequest; +pub use service::CellId; pub use service::CodeModeService; -pub use service::CodeModeTurnHost; -pub use service::CodeModeTurnWorker; +pub use service::CodeModeSession; +pub use service::CodeModeSessionDelegate; +pub use service::CodeModeSessionProvider; +pub use service::CodeModeSessionProviderFuture; +pub use service::CodeModeSessionResultFuture; +pub use service::InProcessCodeModeSessionProvider; +pub use service::NoopCodeModeSessionDelegate; +pub use service::NotificationFuture; +pub use service::StartedCell; +pub use service::ToolInvocationFuture; pub const PUBLIC_TOOL_NAME: &str = "exec"; pub const WAIT_TOOL_NAME: &str = "wait"; diff --git a/codex-rs/code-mode/src/response.rs b/codex-rs/code-mode/src/response.rs index ae92639cc0..0ac3a03770 100644 --- a/codex-rs/code-mode/src/response.rs +++ b/codex-rs/code-mode/src/response.rs @@ -4,6 +4,8 @@ use serde::Serialize; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ImageDetail { + Auto, + Low, High, Original, } diff --git a/codex-rs/code-mode/src/runtime/mod.rs b/codex-rs/code-mode/src/runtime/mod.rs index 89893d2b25..21757328ee 100644 --- a/codex-rs/code-mode/src/runtime/mod.rs +++ b/codex-rs/code-mode/src/runtime/mod.rs @@ -19,6 +19,7 @@ use crate::description::EnabledToolMetadata; use crate::description::ToolDefinition; use crate::description::enabled_tool_metadata; use crate::response::FunctionCallOutputContentItem; +use crate::service::CellId; pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000; pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000; @@ -27,11 +28,6 @@ const EXIT_SENTINEL: &str = "__codex_code_mode_exit__"; #[derive(Clone, Debug)] pub struct ExecuteRequest { - /// Runtime cell id for this execution. - /// - /// Callers allocate this before execution so tracing, waits, and nested tool - /// calls can refer to the cell as soon as JavaScript starts. - pub cell_id: String, pub tool_call_id: String, pub enabled_tools: Vec, pub source: String, @@ -41,14 +37,13 @@ pub struct ExecuteRequest { #[derive(Clone, Debug)] pub struct WaitRequest { - pub cell_id: String, + pub cell_id: CellId, pub yield_time_ms: u64, - pub terminate: bool, } #[derive(Clone, Debug)] pub struct WaitToPendingRequest { - pub cell_id: String, + pub cell_id: CellId, } /// Result of waiting on a code-mode cell. @@ -73,7 +68,7 @@ pub enum ExecuteToPendingOutcome { /// The cell is waiting for more runtime input after draining the runtime /// input queue that was ready at the pending boundary. Pending { - cell_id: String, + cell_id: CellId, content_items: Vec, /// Runtime tool-call ids emitted before this paused execution frontier /// sealed. Hosts can use these ids to drain their tool-call transport @@ -105,15 +100,15 @@ impl From for RuntimeResponse { #[derive(Debug, PartialEq, Serialize)] pub enum RuntimeResponse { Yielded { - cell_id: String, + cell_id: CellId, content_items: Vec, }, Terminated { - cell_id: String, + cell_id: CellId, content_items: Vec, }, Result { - cell_id: String, + cell_id: CellId, content_items: Vec, error_text: Option, }, @@ -126,23 +121,13 @@ pub enum RuntimeResponse { /// if their tool-call graph requires globally unique ids. #[derive(Debug)] pub struct CodeModeNestedToolCall { - pub cell_id: String, + pub cell_id: CellId, pub runtime_tool_call_id: String, pub tool_name: ToolName, pub tool_kind: CodeModeToolKind, pub input: Option, } -#[derive(Debug)] -pub(crate) enum TurnMessage { - ToolCall(CodeModeNestedToolCall), - Notify { - cell_id: String, - call_id: String, - text: String, - }, -} - #[derive(Debug)] pub(crate) enum RuntimeCommand { ToolResponse { id: String, result: JsonValue }, @@ -460,7 +445,6 @@ mod tests { fn execute_request(source: &str) -> ExecuteRequest { ExecuteRequest { - cell_id: "1".to_string(), tool_call_id: "call_1".to_string(), enabled_tools: Vec::new(), source: source.to_string(), diff --git a/codex-rs/code-mode/src/runtime/value.rs b/codex-rs/code-mode/src/runtime/value.rs index 865b5a569d..8d76a832d3 100644 --- a/codex-rs/code-mode/src/runtime/value.rs +++ b/codex-rs/code-mode/src/runtime/value.rs @@ -71,10 +71,14 @@ pub(super) fn normalize_output_image( Some(detail) => { let normalized = detail.to_ascii_lowercase(); Some(match normalized.as_str() { + "auto" => ImageDetail::Auto, + "low" => ImageDetail::Low, "high" => ImageDetail::High, "original" => ImageDetail::Original, _ => { - return Err("image detail must be one of: high, original".to_string()); + return Err( + "image detail must be one of: auto, low, high, original".to_string() + ); } }) } @@ -156,7 +160,7 @@ fn parse_mcp_output_image( .and_then(JsonValue::as_object) .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(JsonValue::as_str) - .filter(|detail| matches!(*detail, "high" | "original")) + .filter(|detail| matches!(*detail, "auto" | "low" | "high" | "original")) .map(str::to_string); Ok((image_url, detail)) } diff --git a/codex-rs/code-mode/src/service.rs b/codex-rs/code-mode/src/service.rs index de4ed13e58..6589e6ec5a 100644 --- a/codex-rs/code-mode/src/service.rs +++ b/codex-rs/code-mode/src/service.rs @@ -1,14 +1,20 @@ use std::collections::HashMap; +use std::fmt; +use std::future::Future; +use std::pin::Pin; use std::sync::Arc; +use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::time::Duration; -use async_trait::async_trait; +use serde::Deserialize; +use serde::Serialize; use serde_json::Value as JsonValue; use tokio::sync::Mutex; use tokio::sync::mpsc; use tokio::sync::oneshot; +use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; use tracing::warn; @@ -22,35 +28,162 @@ use crate::runtime::RuntimeCommand; use crate::runtime::RuntimeControlCommand; use crate::runtime::RuntimeEvent; use crate::runtime::RuntimeResponse; -use crate::runtime::TurnMessage; use crate::runtime::WaitOutcome; use crate::runtime::WaitRequest; use crate::runtime::WaitToPendingOutcome; use crate::runtime::WaitToPendingRequest; use crate::runtime::spawn_runtime; -#[async_trait] -pub trait CodeModeTurnHost: Send + Sync { - async fn invoke_tool( - &self, +pub type CodeModeSessionResultFuture<'a, T> = + Pin> + Send + 'a>>; +pub type CodeModeSessionProviderFuture<'a> = + CodeModeSessionResultFuture<'a, Arc>; +pub type ToolInvocationFuture<'a> = + Pin> + Send + 'a>>; +pub type NotificationFuture<'a> = Pin> + Send + 'a>>; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct CellId(String); + +impl CellId { + pub fn new(value: String) -> Self { + Self(value) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl AsRef for CellId { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl fmt::Display for CellId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +pub struct StartedCell { + pub cell_id: CellId, + initial_response_rx: oneshot::Receiver, +} + +impl StartedCell { + pub async fn initial_response(self) -> Result { + self.initial_response_rx + .await + .map_err(|_| "exec runtime ended unexpectedly".to_string()) + } +} + +/// Host callbacks used by a code-mode session while cells are executing. +pub trait CodeModeSessionDelegate: Send + Sync { + fn invoke_tool<'a>( + &'a self, invocation: CodeModeNestedToolCall, cancellation_token: CancellationToken, - ) -> Result; + ) -> ToolInvocationFuture<'a>; - async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>; + fn notify<'a>( + &'a self, + call_id: String, + cell_id: CellId, + text: String, + cancellation_token: CancellationToken, + ) -> NotificationFuture<'a>; + + /// Releases delegate state associated with a cell after it reaches a terminal state. + fn cell_closed(&self, cell_id: &CellId); +} + +pub struct NoopCodeModeSessionDelegate; + +impl CodeModeSessionDelegate for NoopCodeModeSessionDelegate { + fn invoke_tool<'a>( + &'a self, + _invocation: CodeModeNestedToolCall, + cancellation_token: CancellationToken, + ) -> ToolInvocationFuture<'a> { + Box::pin(async move { + cancellation_token.cancelled().await; + Err("code mode nested tools are unavailable".to_string()) + }) + } + + fn notify<'a>( + &'a self, + _call_id: String, + _cell_id: CellId, + _text: String, + _cancellation_token: CancellationToken, + ) -> NotificationFuture<'a> { + Box::pin(async { Ok(()) }) + } + + fn cell_closed(&self, _cell_id: &CellId) {} +} + +/// A durable code-mode session owned by one Codex thread. +/// +/// Cells executed in the same session share stored values. Separate sessions +/// must keep those values isolated. Implementations may execute cells +/// in-process or remotely. +pub trait CodeModeSession: Send + Sync { + fn execute<'a>( + &'a self, + request: ExecuteRequest, + ) -> CodeModeSessionResultFuture<'a, StartedCell>; + + fn wait<'a>(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome>; + + fn terminate<'a>(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome>; + + fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()>; +} + +/// Creates code-mode sessions for one Codex thread. +/// +/// Providers choose where a session executes and receive the host delegate that +/// the session should use for nested tool calls and notifications. +pub trait CodeModeSessionProvider: Send + Sync { + fn create_session<'a>( + &'a self, + delegate: Arc, + ) -> CodeModeSessionProviderFuture<'a>; +} + +#[derive(Default)] +pub struct InProcessCodeModeSessionProvider; + +impl CodeModeSessionProvider for InProcessCodeModeSessionProvider { + fn create_session<'a>( + &'a self, + delegate: Arc, + ) -> CodeModeSessionProviderFuture<'a> { + Box::pin(async move { + let session: Arc = + Arc::new(CodeModeService::with_delegate(delegate)); + Ok(session) + }) + } } #[derive(Clone)] -struct SessionHandle { - control_tx: mpsc::UnboundedSender, +struct CellHandle { + control_tx: mpsc::UnboundedSender, runtime_tx: std::sync::mpsc::Sender, + cancellation_token: CancellationToken, } struct Inner { stored_values: Mutex>, - sessions: Mutex>, - turn_message_tx: async_channel::Sender, - turn_message_rx: async_channel::Receiver, + cells: Mutex>, + delegate: Arc, + shutting_down: AtomicBool, next_cell_id: AtomicU64, } @@ -60,50 +193,50 @@ pub struct CodeModeService { impl CodeModeService { pub fn new() -> Self { - let (turn_message_tx, turn_message_rx) = async_channel::unbounded(); + Self::with_delegate(Arc::new(NoopCodeModeSessionDelegate)) + } + pub fn with_delegate(delegate: Arc) -> Self { Self { inner: Arc::new(Inner { stored_values: Mutex::new(HashMap::new()), - sessions: Mutex::new(HashMap::new()), - turn_message_tx, - turn_message_rx, + cells: Mutex::new(HashMap::new()), + delegate, + shutting_down: AtomicBool::new(false), next_cell_id: AtomicU64::new(1), }), } } - async fn stored_values(&self) -> HashMap { - self.inner.stored_values.lock().await.clone() + fn allocate_cell_id(&self) -> CellId { + CellId::new( + self.inner + .next_cell_id + .fetch_add(1, Ordering::Relaxed) + .to_string(), + ) } - /// Reserves the runtime cell id for a future `execute` request. - /// - /// The runtime can issue nested tool calls before the first `execute` - /// response is returned. Hosts that need a parent trace object for those - /// nested calls should allocate the cell id up front and pass it back on the - /// `ExecuteRequest`. - pub fn allocate_cell_id(&self) -> String { - self.inner - .next_cell_id - .fetch_add(1, Ordering::Relaxed) - .to_string() - } - - pub async fn execute(&self, request: ExecuteRequest) -> Result { + pub async fn execute(&self, request: ExecuteRequest) -> Result { + if self.inner.shutting_down.load(Ordering::Acquire) { + return Err("code mode session is shutting down".to_string()); + } let initial_yield_time_ms = request.yield_time_ms.unwrap_or(DEFAULT_EXEC_YIELD_TIME_MS); let (response_tx, response_rx) = oneshot::channel(); - self.start_session( + let cell_id = self.allocate_cell_id(); + self.start_cell( + cell_id.clone(), request, - SessionResponseSender::Runtime(response_tx), + CellResponseSender::Runtime(response_tx), Some(initial_yield_time_ms), PendingRuntimeMode::Continue, ) .await?; - response_rx - .await - .map_err(|_| "exec runtime ended unexpectedly".to_string()) + Ok(StartedCell { + cell_id, + initial_response_rx: response_rx, + }) } pub async fn execute_to_pending( @@ -111,9 +244,11 @@ impl CodeModeService { request: ExecuteRequest, ) -> Result { let (response_tx, response_rx) = oneshot::channel(); - self.start_session( + let cell_id = self.allocate_cell_id(); + self.start_cell( + cell_id, request, - SessionResponseSender::ExecuteToPending(response_tx), + CellResponseSender::ExecuteToPending(response_tx), /*initial_yield_time_ms*/ None, PendingRuntimeMode::PauseUntilResumed, ) @@ -124,47 +259,50 @@ impl CodeModeService { .map_err(|_| "exec runtime ended unexpectedly".to_string()) } - async fn start_session( + async fn start_cell( &self, + cell_id: CellId, request: ExecuteRequest, - initial_response_tx: SessionResponseSender, + initial_response_tx: CellResponseSender, initial_yield_time_ms: Option, pending_mode: PendingRuntimeMode, ) -> Result<(), String> { - let cell_id = request.cell_id.clone(); let (event_tx, event_rx) = mpsc::unbounded_channel(); let (control_tx, control_rx) = mpsc::unbounded_channel(); - let stored_values = self.stored_values().await; + let stored_values = self.inner.stored_values.lock().await.clone(); + let cancellation_token = CancellationToken::new(); let (runtime_tx, runtime_control_tx, runtime_terminate_handle) = { - let mut sessions = self.inner.sessions.lock().await; - if sessions.contains_key(&cell_id) { + let mut cells = self.inner.cells.lock().await; + if self.inner.shutting_down.load(Ordering::Acquire) { + return Err("code mode session is shutting down".to_string()); + } + if cells.contains_key(&cell_id) { return Err(format!("exec cell {cell_id} already exists")); } let (runtime_tx, runtime_control_tx, runtime_terminate_handle) = spawn_runtime(stored_values, request, event_tx, pending_mode)?; - // Keep the session registry locked through insertion so a - // caller-owned cell id cannot race with another execute and replace - // a live runtime. - sessions.insert( + cells.insert( cell_id.clone(), - SessionHandle { + CellHandle { control_tx, runtime_tx: runtime_tx.clone(), + cancellation_token: cancellation_token.clone(), }, ); (runtime_tx, runtime_control_tx, runtime_terminate_handle) }; - tokio::spawn(run_session_control( + tokio::spawn(run_cell_control( Arc::clone(&self.inner), - SessionControlContext { - cell_id: cell_id.clone(), + CellControlContext { + cell_id, runtime_tx, runtime_control_tx, pending_mode, runtime_terminate_handle, + cancellation_token, }, event_rx, control_rx, @@ -176,34 +314,44 @@ impl CodeModeService { } pub async fn wait(&self, request: WaitRequest) -> Result { - let cell_id = request.cell_id.clone(); - let handle = self - .inner - .sessions - .lock() - .await - .get(&request.cell_id) - .cloned(); + let WaitRequest { + cell_id, + yield_time_ms, + } = request; + let handle = self.inner.cells.lock().await.get(&cell_id).cloned(); let Some(handle) = handle else { return Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))); }; let (response_tx, response_rx) = oneshot::channel(); - let control_message = if request.terminate { - SessionControlCommand::Terminate { response_tx } - } else { - SessionControlCommand::Poll { - yield_time_ms: request.yield_time_ms, - response_tx, - } + let control_message = CellControlCommand::Poll { + yield_time_ms, + response_tx, }; if handle.control_tx.send(control_message).is_err() { return Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))); } match response_rx.await { Ok(response) => Ok(WaitOutcome::LiveCell(response)), - Err(_) => Ok(WaitOutcome::MissingCell(missing_cell_response( - request.cell_id, - ))), + Err(_) => Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))), + } + } + + pub async fn terminate(&self, cell_id: CellId) -> Result { + let handle = self.inner.cells.lock().await.get(&cell_id).cloned(); + let Some(handle) = handle else { + return Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))); + }; + let (response_tx, response_rx) = oneshot::channel(); + if handle + .control_tx + .send(CellControlCommand::Terminate { response_tx }) + .is_err() + { + return Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))); + } + match response_rx.await { + Ok(response) => Ok(WaitOutcome::LiveCell(response)), + Err(_) => Ok(WaitOutcome::MissingCell(missing_cell_response(cell_id))), } } @@ -211,14 +359,8 @@ impl CodeModeService { &self, request: WaitToPendingRequest, ) -> Result { - let cell_id = request.cell_id.clone(); - let handle = self - .inner - .sessions - .lock() - .await - .get(&request.cell_id) - .cloned(); + let cell_id = request.cell_id; + let handle = self.inner.cells.lock().await.get(&cell_id).cloned(); let Some(handle) = handle else { return Ok(WaitToPendingOutcome::MissingCell(missing_cell_response( cell_id, @@ -227,7 +369,7 @@ impl CodeModeService { let (response_tx, response_rx) = oneshot::channel(); if handle .control_tx - .send(SessionControlCommand::PollToPending { response_tx }) + .send(CellControlCommand::PollToPending { response_tx }) .is_err() { return Ok(WaitToPendingOutcome::MissingCell(missing_cell_response( @@ -237,74 +379,33 @@ impl CodeModeService { match response_rx.await { Ok(response) => Ok(WaitToPendingOutcome::LiveCell(response)), Err(_) => Ok(WaitToPendingOutcome::MissingCell(missing_cell_response( - request.cell_id, + cell_id, ))), } } - pub fn start_turn_worker(&self, host: Arc) -> CodeModeTurnWorker { - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let inner = Arc::clone(&self.inner); - let turn_message_rx = self.inner.turn_message_rx.clone(); - - tokio::spawn(async move { - loop { - let next_message = tokio::select! { - _ = &mut shutdown_rx => break, - message = turn_message_rx.recv() => message.ok(), - }; - let Some(next_message) = next_message else { - break; - }; - match next_message { - TurnMessage::Notify { - cell_id, - call_id, - text, - } => { - if let Err(err) = host.notify(call_id, cell_id.clone(), text).await { - warn!( - "failed to deliver code mode notification for cell {cell_id}: {err}" - ); - } - } - TurnMessage::ToolCall(invocation) => { - let host = Arc::clone(&host); - let inner = Arc::clone(&inner); - tokio::spawn(async move { - let cell_id = invocation.cell_id.clone(); - let runtime_tool_call_id = invocation.runtime_tool_call_id.clone(); - let response = - host.invoke_tool(invocation, CancellationToken::new()).await; - let runtime_tx = inner - .sessions - .lock() - .await - .get(&cell_id) - .map(|handle| handle.runtime_tx.clone()); - let Some(runtime_tx) = runtime_tx else { - return; - }; - let command = match response { - Ok(result) => RuntimeCommand::ToolResponse { - id: runtime_tool_call_id, - result, - }, - Err(error_text) => RuntimeCommand::ToolError { - id: runtime_tool_call_id, - error_text, - }, - }; - let _ = runtime_tx.send(command); - }); - } - } - } - }); - - CodeModeTurnWorker { - shutdown_tx: Some(shutdown_tx), + pub async fn shutdown(&self) -> Result<(), String> { + self.inner.shutting_down.store(true, Ordering::Release); + let handles = self + .inner + .cells + .lock() + .await + .values() + .cloned() + .collect::>(); + for handle in handles { + handle.cancellation_token.cancel(); + let (response_tx, _response_rx) = oneshot::channel(); + let _ = handle + .control_tx + .send(CellControlCommand::Terminate { response_tx }); + let _ = handle.runtime_tx.send(RuntimeCommand::Terminate); } + while !self.inner.cells.lock().await.is_empty() { + tokio::task::yield_now().await; + } + Ok(()) } } @@ -314,19 +415,44 @@ impl Default for CodeModeService { } } -pub struct CodeModeTurnWorker { - shutdown_tx: Option>, -} - -impl Drop for CodeModeTurnWorker { +impl Drop for CodeModeService { fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); + self.inner.shutting_down.store(true, Ordering::Release); + if let Ok(cells) = self.inner.cells.try_lock() { + for handle in cells.values() { + handle.cancellation_token.cancel(); + let (response_tx, _response_rx) = oneshot::channel(); + let _ = handle + .control_tx + .send(CellControlCommand::Terminate { response_tx }); + let _ = handle.runtime_tx.send(RuntimeCommand::Terminate); + } } } } -enum SessionControlCommand { +impl CodeModeSession for CodeModeService { + fn execute<'a>( + &'a self, + request: ExecuteRequest, + ) -> CodeModeSessionResultFuture<'a, StartedCell> { + Box::pin(CodeModeService::execute(self, request)) + } + + fn wait<'a>(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome> { + Box::pin(CodeModeService::wait(self, request)) + } + + fn terminate<'a>(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome> { + Box::pin(CodeModeService::terminate(self, cell_id)) + } + + fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()> { + Box::pin(CodeModeService::shutdown(self)) + } +} + +enum CellControlCommand { Poll { yield_time_ms: u64, response_tx: oneshot::Sender, @@ -339,7 +465,7 @@ enum SessionControlCommand { }, } -enum SessionResponseSender { +enum CellResponseSender { Runtime(oneshot::Sender), ExecuteToPending(oneshot::Sender), } @@ -349,15 +475,16 @@ struct PendingResult { error_text: Option, } -struct SessionControlContext { - cell_id: String, +struct CellControlContext { + cell_id: CellId, runtime_tx: std::sync::mpsc::Sender, runtime_control_tx: std::sync::mpsc::Sender, pending_mode: PendingRuntimeMode, runtime_terminate_handle: v8::IsolateHandle, + cancellation_token: CancellationToken, } -fn missing_cell_response(cell_id: String) -> RuntimeResponse { +fn missing_cell_response(cell_id: CellId) -> RuntimeResponse { RuntimeResponse::Result { error_text: Some(format!("exec cell {cell_id} not found")), cell_id, @@ -365,29 +492,29 @@ fn missing_cell_response(cell_id: String) -> RuntimeResponse { } } -fn pending_result_response(cell_id: &str, result: PendingResult) -> RuntimeResponse { +fn pending_result_response(cell_id: &CellId, result: PendingResult) -> RuntimeResponse { RuntimeResponse::Result { - cell_id: cell_id.to_string(), + cell_id: cell_id.clone(), content_items: result.content_items, error_text: result.error_text, } } -fn send_terminal_response(response_tx: SessionResponseSender, response: RuntimeResponse) { +fn send_terminal_response(response_tx: CellResponseSender, response: RuntimeResponse) { match response_tx { - SessionResponseSender::Runtime(response_tx) => { + CellResponseSender::Runtime(response_tx) => { let _ = response_tx.send(response); } - SessionResponseSender::ExecuteToPending(response_tx) => { + CellResponseSender::ExecuteToPending(response_tx) => { let _ = response_tx.send(ExecuteToPendingOutcome::Completed(response)); } } } fn send_or_buffer_result( - cell_id: &str, + cell_id: &CellId, result: PendingResult, - response_tx: &mut Option, + response_tx: &mut Option, pending_result: &mut Option, ) -> bool { if let Some(response_tx) = response_tx.take() { @@ -401,42 +528,41 @@ fn send_or_buffer_result( } fn send_yield_response( - cell_id: &str, + cell_id: &CellId, content_items: &mut Vec, - response_tx: &mut Option, + response_tx: &mut Option, ) { let Some(current_response_tx) = response_tx.take() else { return; }; match current_response_tx { - SessionResponseSender::Runtime(response_tx) => { + CellResponseSender::Runtime(response_tx) => { let _ = response_tx.send(RuntimeResponse::Yielded { - cell_id: cell_id.to_string(), + cell_id: cell_id.clone(), content_items: std::mem::take(content_items), }); } - SessionResponseSender::ExecuteToPending(execute_to_pending_tx) => { - *response_tx = Some(SessionResponseSender::ExecuteToPending( - execute_to_pending_tx, - )); + CellResponseSender::ExecuteToPending(execute_to_pending_tx) => { + *response_tx = Some(CellResponseSender::ExecuteToPending(execute_to_pending_tx)); } } } -async fn run_session_control( +async fn run_cell_control( inner: Arc, - context: SessionControlContext, + context: CellControlContext, mut event_rx: mpsc::UnboundedReceiver, - mut control_rx: mpsc::UnboundedReceiver, - initial_response_tx: SessionResponseSender, + mut control_rx: mpsc::UnboundedReceiver, + initial_response_tx: CellResponseSender, initial_yield_time_ms: Option, ) { - let SessionControlContext { + let CellControlContext { cell_id, runtime_tx, runtime_control_tx, pending_mode, runtime_terminate_handle, + cancellation_token, } = context; let mut content_items = Vec::new(); let mut pending_tool_call_ids = Vec::new(); @@ -445,6 +571,7 @@ async fn run_session_control( let mut termination_requested = false; let mut runtime_closed = false; let mut yield_timer: Option>> = None; + let mut notification_tasks = JoinSet::new(); loop { tokio::select! { @@ -492,11 +619,11 @@ async fn run_session_control( RuntimeEvent::Pending => { if let Some(current_response_tx) = response_tx.take() { match current_response_tx { - SessionResponseSender::Runtime(runtime_response_tx) => { + CellResponseSender::Runtime(runtime_response_tx) => { response_tx = - Some(SessionResponseSender::Runtime(runtime_response_tx)); + Some(CellResponseSender::Runtime(runtime_response_tx)); } - SessionResponseSender::ExecuteToPending(response_tx) => { + CellResponseSender::ExecuteToPending(response_tx) => { let _ = response_tx.send(ExecuteToPendingOutcome::Pending { cell_id: cell_id.clone(), content_items: std::mem::take(&mut content_items), @@ -516,11 +643,26 @@ async fn run_session_control( send_yield_response(&cell_id, &mut content_items, &mut response_tx); } RuntimeEvent::Notify { call_id, text } => { - let _ = inner.turn_message_tx.send(TurnMessage::Notify { - cell_id: cell_id.clone(), - call_id, - text, - }).await; + let delegate = Arc::clone(&inner.delegate); + let cell_id = cell_id.clone(); + let cancellation_token = cancellation_token.child_token(); + notification_tasks.spawn(async move { + tokio::select! { + result = delegate.notify( + call_id, + cell_id.clone(), + text, + cancellation_token.clone(), + ) => { + if let Err(err) = result { + warn!( + "failed to deliver code mode notification for cell {cell_id}: {err}" + ); + } + } + _ = cancellation_token.cancelled() => {} + } + }); } RuntimeEvent::ToolCall { id, @@ -533,15 +675,25 @@ async fn run_session_control( } let tool_call = CodeModeNestedToolCall { cell_id: cell_id.clone(), - runtime_tool_call_id: id, + runtime_tool_call_id: id.clone(), tool_name: name, tool_kind: kind, input, }; - let _ = inner - .turn_message_tx - .send(TurnMessage::ToolCall(tool_call)) - .await; + let delegate = Arc::clone(&inner.delegate); + let runtime_tx = runtime_tx.clone(); + let cancellation_token = cancellation_token.child_token(); + tokio::spawn(async move { + let response = tokio::select! { + response = delegate.invoke_tool(tool_call, cancellation_token.clone()) => response, + _ = cancellation_token.cancelled() => return, + }; + let command = match response { + Ok(result) => RuntimeCommand::ToolResponse { id, result }, + Err(error_text) => RuntimeCommand::ToolError { id, error_text }, + }; + let _ = runtime_tx.send(command); + }); } RuntimeEvent::Result { stored_value_writes, @@ -558,6 +710,7 @@ async fn run_session_control( } break; } + drain_notification_tasks(&mut notification_tasks).await; inner .stored_values .lock() @@ -578,12 +731,19 @@ async fn run_session_control( } } } + task_result = notification_tasks.join_next(), if !notification_tasks.is_empty() => { + if let Some(Err(err)) = task_result + && !err.is_cancelled() + { + warn!("code mode notification task failed: {err}"); + } + } maybe_command = control_rx.recv() => { let Some(command) = maybe_command else { break; }; match command { - SessionControlCommand::Poll { + CellControlCommand::Poll { yield_time_ms, response_tx: next_response_tx, } => { @@ -591,11 +751,11 @@ async fn run_session_control( let _ = next_response_tx.send(pending_result_response(&cell_id, result)); break; } - response_tx = Some(SessionResponseSender::Runtime(next_response_tx)); + response_tx = Some(CellResponseSender::Runtime(next_response_tx)); yield_timer = Some(Box::pin(tokio::time::sleep(Duration::from_millis(yield_time_ms)))); resume_paused_runtime(&runtime_control_tx, pending_mode); } - SessionControlCommand::PollToPending { + CellControlCommand::PollToPending { response_tx: next_response_tx, } => { if let Some(result) = pending_result.take() { @@ -605,18 +765,19 @@ async fn run_session_control( break; } response_tx = - Some(SessionResponseSender::ExecuteToPending(next_response_tx)); + Some(CellResponseSender::ExecuteToPending(next_response_tx)); yield_timer = None; resume_paused_runtime(&runtime_control_tx, pending_mode); } - SessionControlCommand::Terminate { response_tx: next_response_tx } => { + CellControlCommand::Terminate { response_tx: next_response_tx } => { if let Some(result) = pending_result.take() { let _ = next_response_tx.send(pending_result_response(&cell_id, result)); break; } - response_tx = Some(SessionResponseSender::Runtime(next_response_tx)); + response_tx = Some(CellResponseSender::Runtime(next_response_tx)); termination_requested = true; + cancellation_token.cancel(); yield_timer = None; let _ = runtime_tx.send(RuntimeCommand::Terminate); terminate_paused_runtime(&runtime_control_tx, pending_mode); @@ -650,8 +811,21 @@ async fn run_session_control( } let _ = runtime_tx.send(RuntimeCommand::Terminate); + cancellation_token.cancel(); + drain_notification_tasks(&mut notification_tasks).await; terminate_paused_runtime(&runtime_control_tx, pending_mode); - inner.sessions.lock().await.remove(&cell_id); + inner.cells.lock().await.remove(&cell_id); + inner.delegate.cell_closed(&cell_id); +} + +async fn drain_notification_tasks(notification_tasks: &mut JoinSet<()>) { + while let Some(result) = notification_tasks.join_next().await { + if let Err(err) = result + && !err.is_cancelled() + { + warn!("code mode notification task failed: {err}"); + } + } } fn resume_paused_runtime( @@ -677,6 +851,7 @@ mod tests { use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicU64; + use std::sync::atomic::Ordering; use std::time::Duration; use codex_protocol::ToolName; @@ -685,19 +860,21 @@ mod tests { use tokio::sync::mpsc; use tokio::sync::oneshot; + use super::CellControlCommand; + use super::CellControlContext; + use super::CellId; + use super::CellResponseSender; use super::CodeModeService; use super::Inner; + use super::NoopCodeModeSessionDelegate; use super::PendingRuntimeMode; use super::RuntimeCommand; use super::RuntimeResponse; - use super::SessionControlCommand; - use super::SessionControlContext; - use super::SessionResponseSender; use super::WaitOutcome; use super::WaitRequest; use super::WaitToPendingOutcome; use super::WaitToPendingRequest; - use super::run_session_control; + use super::run_cell_control; use crate::CodeModeToolKind; use crate::FunctionCallOutputContentItem; use crate::ToolDefinition; @@ -708,7 +885,6 @@ mod tests { fn execute_request(source: &str) -> ExecuteRequest { ExecuteRequest { - cell_id: "1".to_string(), tool_call_id: "call_1".to_string(), enabled_tools: Vec::new(), source: source.to_string(), @@ -717,13 +893,26 @@ mod tests { } } + fn cell_id(value: &str) -> CellId { + CellId::new(value.to_string()) + } + + async fn execute(service: &CodeModeService, request: ExecuteRequest) -> RuntimeResponse { + service + .execute(request) + .await + .unwrap() + .initial_response() + .await + .unwrap() + } + fn test_inner() -> Arc { - let (turn_message_tx, turn_message_rx) = async_channel::unbounded(); Arc::new(Inner { stored_values: Mutex::new(HashMap::new()), - sessions: Mutex::new(HashMap::new()), - turn_message_tx, - turn_message_rx, + cells: Mutex::new(HashMap::new()), + delegate: Arc::new(NoopCodeModeSessionDelegate), + shutting_down: std::sync::atomic::AtomicBool::new(false), next_cell_id: AtomicU64::new(1), }) } @@ -732,19 +921,20 @@ mod tests { async fn synchronous_exit_returns_successfully() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#"text("before"); exit(); text("after");"#.to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "before".to_string(), }], @@ -753,6 +943,116 @@ mod tests { ); } + #[tokio::test] + async fn stored_values_are_shared_between_cells_but_not_sessions() { + let first_session = CodeModeService::new(); + let second_session = CodeModeService::new(); + + let write_response = execute( + &first_session, + ExecuteRequest { + source: r#"store("key", "visible");"#.to_string(), + yield_time_ms: None, + ..execute_request("") + }, + ) + .await; + + let same_session = execute( + &first_session, + ExecuteRequest { + source: r#"text(String(load("key")));"#.to_string(), + yield_time_ms: None, + ..execute_request("") + }, + ) + .await; + let other_session = execute( + &second_session, + ExecuteRequest { + source: r#"text(String(load("key")));"#.to_string(), + yield_time_ms: None, + ..execute_request("") + }, + ) + .await; + + assert_eq!( + write_response, + RuntimeResponse::Result { + cell_id: cell_id("1"), + content_items: Vec::new(), + error_text: None, + } + ); + assert_eq!( + same_session, + RuntimeResponse::Result { + cell_id: cell_id("2"), + content_items: vec![FunctionCallOutputContentItem::InputText { + text: "visible".to_string(), + }], + error_text: None, + } + ); + assert_eq!( + other_session, + RuntimeResponse::Result { + cell_id: cell_id("1"), + content_items: vec![FunctionCallOutputContentItem::InputText { + text: "undefined".to_string(), + }], + error_text: None, + } + ); + } + + #[tokio::test] + async fn shutdown_interrupts_cpu_bound_cells() { + let service = CodeModeService::new(); + + let cell = service + .execute(ExecuteRequest { + source: "while (true) {}".to_string(), + ..execute_request("") + }) + .await + .unwrap(); + assert_eq!( + cell.initial_response().await.unwrap(), + RuntimeResponse::Yielded { + cell_id: cell_id("1"), + content_items: Vec::new(), + } + ); + + tokio::time::timeout(Duration::from_secs(1), service.shutdown()) + .await + .unwrap() + .unwrap(); + } + + #[tokio::test] + async fn start_cell_rejects_new_cell_after_shutdown_begins() { + let service = CodeModeService::new(); + service.inner.shutting_down.store(true, Ordering::Release); + let (response_tx, _response_rx) = oneshot::channel(); + + let error = service + .start_cell( + cell_id("late-cell"), + execute_request(""), + CellResponseSender::Runtime(response_tx), + Some(/*initial_yield_time_ms*/ 1), + PendingRuntimeMode::Continue, + ) + .await + .unwrap_err(); + + assert_eq!(error, "code mode session is shutting down".to_string()); + assert!(service.inner.cells.lock().await.is_empty()); + } + #[tokio::test] async fn execute_to_pending_returns_completed_for_synchronous_results() { let service = CodeModeService::new(); @@ -769,7 +1069,7 @@ mod tests { assert_eq!( response, ExecuteToPendingOutcome::Completed(RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "done".to_string(), }], @@ -797,7 +1097,7 @@ mod tests { assert_eq!( response, ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "before".to_string(), }], @@ -805,19 +1105,12 @@ mod tests { } ); - let termination = service - .wait(WaitRequest { - cell_id: "1".to_string(), - yield_time_ms: 1, - terminate: true, - }) - .await - .unwrap(); + let termination = service.terminate(cell_id("1")).await.unwrap(); assert_eq!( termination, WaitOutcome::LiveCell(RuntimeResponse::Terminated { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), }) ); @@ -853,25 +1146,18 @@ await Promise.all([ assert_eq!( response, ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), pending_tool_call_ids: vec!["tool-1".to_string(), "tool-2".to_string()], } ); - let termination = service - .wait(WaitRequest { - cell_id: "1".to_string(), - yield_time_ms: 1, - terminate: true, - }) - .await - .unwrap(); + let termination = service.terminate(cell_id("1")).await.unwrap(); assert_eq!( termination, WaitOutcome::LiveCell(RuntimeResponse::Terminated { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), }) ); @@ -910,7 +1196,7 @@ await Promise.all([ assert_eq!( initial_response, ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), pending_tool_call_ids: vec!["tool-1".to_string(), "tool-2".to_string()], } @@ -918,10 +1204,10 @@ await Promise.all([ let runtime_tx = service .inner - .sessions + .cells .lock() .await - .get("1") + .get(&cell_id("1")) .unwrap() .runtime_tx .clone(); @@ -932,7 +1218,7 @@ await Promise.all([ let resumed_response = tokio::time::timeout( Duration::from_secs(1), service.wait_to_pending(WaitToPendingRequest { - cell_id: "1".to_string(), + cell_id: cell_id("1"), }), ) .await @@ -942,25 +1228,18 @@ await Promise.all([ assert_eq!( resumed_response, WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), pending_tool_call_ids: vec!["tool-3".to_string()], }) ); - let termination = service - .wait(WaitRequest { - cell_id: "1".to_string(), - yield_time_ms: 1, - terminate: true, - }) - .await - .unwrap(); + let termination = service.terminate(cell_id("1")).await.unwrap(); assert_eq!( termination, WaitOutcome::LiveCell(RuntimeResponse::Terminated { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), }) ); @@ -987,7 +1266,7 @@ await new Promise(() => {}); assert_eq!( initial_response, ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), pending_tool_call_ids: Vec::new(), } @@ -995,10 +1274,10 @@ await new Promise(() => {}); let runtime_tx = service .inner - .sessions + .cells .lock() .await - .get("1") + .get(&cell_id("1")) .unwrap() .runtime_tx .clone(); @@ -1009,7 +1288,7 @@ await new Promise(() => {}); let resumed_response = tokio::time::timeout( Duration::from_secs(1), service.wait_to_pending(WaitToPendingRequest { - cell_id: "1".to_string(), + cell_id: cell_id("1"), }), ) .await @@ -1019,7 +1298,7 @@ await new Promise(() => {}); assert_eq!( resumed_response, WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "after".to_string(), }], @@ -1027,19 +1306,12 @@ await new Promise(() => {}); }) ); - let termination = service - .wait(WaitRequest { - cell_id: "1".to_string(), - yield_time_ms: 1, - terminate: true, - }) - .await - .unwrap(); + let termination = service.terminate(cell_id("1")).await.unwrap(); assert_eq!( termination, WaitOutcome::LiveCell(RuntimeResponse::Terminated { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), }) ); @@ -1065,7 +1337,7 @@ text("done"); assert_eq!( initial_response, ExecuteToPendingOutcome::Pending { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), pending_tool_call_ids: Vec::new(), } @@ -1073,10 +1345,10 @@ text("done"); let runtime_tx = service .inner - .sessions + .cells .lock() .await - .get("1") + .get(&cell_id("1")) .unwrap() .runtime_tx .clone(); @@ -1087,7 +1359,7 @@ text("done"); let resumed_response = tokio::time::timeout( Duration::from_secs(1), service.wait_to_pending(WaitToPendingRequest { - cell_id: "1".to_string(), + cell_id: cell_id("1"), }), ) .await @@ -1098,7 +1370,7 @@ text("done"); resumed_response, WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Completed( RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "done".to_string(), }], @@ -1112,19 +1384,20 @@ text("done"); async fn v8_console_is_not_exposed_on_global_this() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#"text(String(Object.hasOwn(globalThis, "console")));"#.to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "false".to_string(), }], @@ -1137,8 +1410,9 @@ text("done"); async fn date_locale_string_formats_with_icu_data() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" const value = new Date("2025-01-02T03:04:05Z") .toLocaleString("fr-FR", { @@ -1156,14 +1430,14 @@ text(value); .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "jeudi 2 janvier \u{e0} 03:04:05".to_string(), }], @@ -1176,8 +1450,9 @@ text(value); async fn intl_date_time_format_formats_with_icu_data() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" const formatter = new Intl.DateTimeFormat("fr-FR", { weekday: "long", @@ -1194,14 +1469,14 @@ text(formatter.format(new Date("2025-01-02T03:04:05Z"))); .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputText { text: "jeudi 2 janvier \u{e0} 03:04:05".to_string(), }], @@ -1214,8 +1489,9 @@ text(formatter.format(new Date("2025-01-02T03:04:05Z"))); async fn output_helpers_return_undefined() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" const returnsUndefined = [ text("first"), @@ -1227,14 +1503,14 @@ text(JSON.stringify(returnsUndefined)); .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![ FunctionCallOutputContentItem::InputText { text: "first".to_string(), @@ -1256,8 +1532,9 @@ text(JSON.stringify(returnsUndefined)); async fn image_helper_accepts_raw_mcp_image_block_with_original_detail() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" image({ type: "image", @@ -1269,14 +1546,14 @@ image({ .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), detail: Some(crate::ImageDetail::Original), @@ -1290,8 +1567,9 @@ image({ async fn image_helper_second_arg_overrides_explicit_object_detail() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" image( { @@ -1304,14 +1582,14 @@ image( .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputImage { image_url: "https://example.com/image.jpg".to_string(), detail: Some(crate::ImageDetail::Original), @@ -1325,8 +1603,9 @@ image( async fn image_helper_second_arg_overrides_raw_mcp_image_detail() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" image( { @@ -1341,14 +1620,14 @@ image( .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), detail: Some(crate::ImageDetail::High), @@ -1359,11 +1638,12 @@ image( } #[tokio::test] - async fn image_helper_rejects_unsupported_detail() { + async fn image_helper_accepts_low_detail() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" image({ image_url: "https://example.com/image.jpg", @@ -1373,16 +1653,51 @@ image({ .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), + content_items: vec![FunctionCallOutputContentItem::InputImage { + image_url: "https://example.com/image.jpg".to_string(), + detail: Some(crate::ImageDetail::Low), + }], + error_text: None, + } + ); + } + + #[tokio::test] + async fn image_helper_rejects_unsupported_detail() { + let service = CodeModeService::new(); + + let response = execute( + &service, + ExecuteRequest { + source: r#" +image({ + image_url: "https://example.com/image.jpg", + detail: "medium", +}); +"# + .to_string(), + yield_time_ms: None, + ..execute_request("") + }, + ) + .await; + + assert_eq!( + response, + RuntimeResponse::Result { + cell_id: cell_id("1"), content_items: Vec::new(), - error_text: Some("image detail must be one of: high, original".to_string()), + error_text: Some( + "image detail must be one of: auto, low, high, original".to_string() + ), } ); } @@ -1391,8 +1706,9 @@ image({ async fn image_helper_rejects_raw_mcp_result_container() { let service = CodeModeService::new(); - let response = service - .execute(ExecuteRequest { + let response = execute( + &service, + ExecuteRequest { source: r#" image({ content: [ @@ -1409,14 +1725,14 @@ image({ .to_string(), yield_time_ms: None, ..execute_request("") - }) - .await - .unwrap(); + }, + ) + .await; assert_eq!( response, RuntimeResponse::Result { - cell_id: "1".to_string(), + cell_id: cell_id("1"), content_items: Vec::new(), error_text: Some( "image expects a non-empty image URL string, an object with image_url and optional detail, or a raw MCP image block".to_string(), @@ -1431,9 +1747,8 @@ image({ let response = service .wait(WaitRequest { - cell_id: "missing".to_string(), + cell_id: cell_id("missing"), yield_time_ms: 1, - terminate: false, }) .await .unwrap(); @@ -1441,7 +1756,7 @@ image({ assert_eq!( response, WaitOutcome::MissingCell(RuntimeResponse::Result { - cell_id: "missing".to_string(), + cell_id: cell_id("missing"), content_items: Vec::new(), error_text: Some("exec cell missing not found".to_string()), }) @@ -1467,18 +1782,19 @@ image({ ) .unwrap(); - tokio::spawn(run_session_control( + tokio::spawn(run_cell_control( inner, - SessionControlContext { - cell_id: "cell-1".to_string(), + CellControlContext { + cell_id: cell_id("cell-1"), runtime_tx: runtime_tx.clone(), runtime_control_tx, pending_mode: PendingRuntimeMode::Continue, runtime_terminate_handle, + cancellation_token: tokio_util::sync::CancellationToken::new(), }, event_rx, control_rx, - SessionResponseSender::Runtime(initial_response_tx), + CellResponseSender::Runtime(initial_response_tx), Some(/*initial_yield_time_ms*/ 60_000), )); @@ -1487,14 +1803,14 @@ image({ assert_eq!( initial_response_rx.await.unwrap(), RuntimeResponse::Yielded { - cell_id: "cell-1".to_string(), + cell_id: cell_id("cell-1"), content_items: Vec::new(), } ); let (terminate_response_tx, terminate_response_rx) = oneshot::channel(); control_tx - .send(SessionControlCommand::Terminate { + .send(CellControlCommand::Terminate { response_tx: terminate_response_tx, }) .unwrap(); @@ -1511,7 +1827,7 @@ image({ assert_eq!( terminate_response.await, RuntimeResponse::Terminated { - cell_id: "cell-1".to_string(), + cell_id: cell_id("cell-1"), content_items: Vec::new(), } ); diff --git a/codex-rs/codex-api/Cargo.toml b/codex-rs/codex-api/Cargo.toml index 08f70cf33c..07d855725a 100644 --- a/codex-rs/codex-api/Cargo.toml +++ b/codex-rs/codex-api/Cargo.toml @@ -16,6 +16,7 @@ codex-utils-rustls-provider = { workspace = true } futures = { workspace = true } http = { workspace = true } reqwest = { workspace = true, features = ["json", "stream"] } +schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs index 2940f872d8..d01fbfb780 100644 --- a/codex-rs/codex-api/src/endpoint/search.rs +++ b/codex-rs/codex-api/src/endpoint/search.rs @@ -145,7 +145,7 @@ mod tests { .search( &SearchRequest { id: "search-session".to_string(), - model: Some("gpt-test".to_string()), + model: "gpt-test".to_string(), reasoning: None, input: Some(SearchInput::Items(vec![ResponseItem::Message { id: None, diff --git a/codex-rs/codex-api/src/search.rs b/codex-rs/codex-api/src/search.rs index b841d06a30..061b3ac8c6 100644 --- a/codex-rs/codex-api/src/search.rs +++ b/codex-rs/codex-api/src/search.rs @@ -1,13 +1,13 @@ use crate::common::Reasoning; use codex_protocol::models::ResponseItem; +use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; #[derive(Debug, Clone, Serialize, PartialEq)] pub struct SearchRequest { pub id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, + pub model: String, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -27,75 +27,100 @@ pub enum SearchInput { Items(Vec), } -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, JsonSchema)] pub struct SearchCommands { + /// Query the internet search engine for a given list of queries. #[serde(skip_serializing_if = "Option::is_none")] pub search_query: Option>, + /// Query the image search engine for a given list of queries. #[serde(skip_serializing_if = "Option::is_none")] pub image_query: Option>, + /// Open pages by reference id or URL. #[serde(skip_serializing_if = "Option::is_none")] pub open: Option>, + /// Open links from previously opened pages. #[serde(skip_serializing_if = "Option::is_none")] pub click: Option>, + /// Find text patterns in pages. #[serde(skip_serializing_if = "Option::is_none")] pub find: Option>, + /// Take screenshots of PDF pages. #[serde(skip_serializing_if = "Option::is_none")] pub screenshot: Option>, + /// Look up prices for the given stock symbols. #[serde(skip_serializing_if = "Option::is_none")] pub finance: Option>, + /// Look up weather forecasts. #[serde(skip_serializing_if = "Option::is_none")] pub weather: Option>, + /// Look up sports schedules and standings. #[serde(skip_serializing_if = "Option::is_none")] pub sports: Option>, + /// Get time for the given UTC offsets. #[serde(skip_serializing_if = "Option::is_none")] pub time: Option>, + /// Set the length of the response to be returned. #[serde(skip_serializing_if = "Option::is_none")] pub response_length: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct SearchQuery { + /// Search query. pub q: String, + /// Whether to filter by recency, as a number of recent days. #[serde(skip_serializing_if = "Option::is_none")] pub recency: Option, + /// Whether to filter by a specific list of domains. #[serde(skip_serializing_if = "Option::is_none")] pub domains: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct OpenOperation { + /// Reference id or URL to open. pub ref_id: String, + /// Line number to position the page at. #[serde(skip_serializing_if = "Option::is_none")] pub lineno: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct ClickOperation { + /// Reference id containing the numbered link. pub ref_id: String, + /// Numbered link id to open. pub id: u64, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct FindOperation { + /// Reference id or URL to search within. pub ref_id: String, + /// Text pattern to find. pub pattern: String, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct ScreenshotOperation { + /// Reference id or URL to screenshot. pub ref_id: String, + /// Zero-indexed PDF page number. pub pageno: u64, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct FinanceOperation { + /// Ticker symbol to look up. pub ticker: String, + /// Asset type to look up. pub r#type: FinanceAssetType, + /// ISO 3166-1 alpha-3 country code, "OTC", or "" for cryptocurrency. #[serde(skip_serializing_if = "Option::is_none")] pub market: Option, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum FinanceAssetType { Equity, @@ -104,49 +129,61 @@ pub enum FinanceAssetType { Index, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct WeatherOperation { + /// Location in "Country, Area, City" format. pub location: String, + /// Start date in YYYY-MM-DD format. Defaults to today. #[serde(skip_serializing_if = "Option::is_none")] pub start: Option, + /// Number of days to return. Defaults to 7. #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct SportsOperation { + /// Tool name for sports requests. #[serde(skip_serializing_if = "Option::is_none")] pub tool: Option, + /// Sports function to call. pub r#fn: SportsFunction, + /// League to look up. pub league: SportsLeague, + /// Team to look up, using the common 3 or 4 letter alias used in broadcasts. #[serde(skip_serializing_if = "Option::is_none")] pub team: Option, + /// Opponent to use with `team` when narrowing the lookup. #[serde(skip_serializing_if = "Option::is_none")] pub opponent: Option, + /// Start date in YYYY-MM-DD format. #[serde(skip_serializing_if = "Option::is_none")] pub date_from: Option, + /// End date in YYYY-MM-DD format. #[serde(skip_serializing_if = "Option::is_none")] pub date_to: Option, + /// Number of games to return. #[serde(skip_serializing_if = "Option::is_none")] pub num_games: Option, + /// Locale for the lookup. #[serde(skip_serializing_if = "Option::is_none")] pub locale: Option, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum SportsToolName { Sports, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum SportsFunction { Schedule, Standings, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum SportsLeague { Nba, @@ -160,12 +197,13 @@ pub enum SportsLeague { Ipl, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct TimeOperation { + /// UTC offset formatted like "+03:00". pub utc_offset: String, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum SearchResponseLength { Short, diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index b1f6f6d4bb..742b39d5fd 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -98,6 +98,8 @@ async fn models_client_hits_models_endpoint() { input_modalities: default_input_modalities(), used_fallback_model_metadata: false, supports_search_tool: false, + auto_review_model_override: None, + tool_mode: None, }], }; diff --git a/codex-rs/codex-backend-openapi-models/src/models/config_bundle_response.rs b/codex-rs/codex-backend-openapi-models/src/models/config_bundle_response.rs new file mode 100644 index 0000000000..9337d57a2a --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/config_bundle_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 crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConfigBundleResponse { + #[serde( + rename = "config_toml", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub config_toml: Option>>, + #[serde( + rename = "requirements_toml", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub requirements_toml: Option>>, +} + +impl ConfigBundleResponse { + pub fn new() -> ConfigBundleResponse { + ConfigBundleResponse { + config_toml: None, + requirements_toml: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs new file mode 100644 index 0000000000..0183eced47 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs @@ -0,0 +1,32 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DeliveredConfigToml { + #[serde( + rename = "enterprise_managed", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub enterprise_managed: Option>>, +} + +impl DeliveredConfigToml { + pub fn new() -> DeliveredConfigToml { + DeliveredConfigToml { + enterprise_managed: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs new file mode 100644 index 0000000000..953087d734 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs @@ -0,0 +1,32 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DeliveredRequirementsToml { + #[serde( + rename = "enterprise_managed", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub enterprise_managed: Option>>, +} + +impl DeliveredRequirementsToml { + pub fn new() -> DeliveredRequirementsToml { + DeliveredRequirementsToml { + enterprise_managed: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_toml_fragment.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_toml_fragment.rs new file mode 100644 index 0000000000..4d44a0e1f7 --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_toml_fragment.rs @@ -0,0 +1,28 @@ +/* + * 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 DeliveredTomlFragment { + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "name")] + pub name: String, + #[serde(rename = "contents")] + pub contents: String, +} + +impl DeliveredTomlFragment { + pub fn new(id: String, name: String, contents: String) -> DeliveredTomlFragment { + DeliveredTomlFragment { id, name, contents } + } +} 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 c881822b37..f0504a9c82 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/mod.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/mod.rs @@ -4,9 +4,21 @@ // The process for this will change // Config +pub(crate) mod config_bundle_response; +pub use self::config_bundle_response::ConfigBundleResponse; + pub(crate) mod config_file_response; pub use self::config_file_response::ConfigFileResponse; +pub(crate) mod delivered_config_toml; +pub use self::delivered_config_toml::DeliveredConfigToml; + +pub(crate) mod delivered_requirements_toml; +pub use self::delivered_requirements_toml::DeliveredRequirementsToml; + +pub(crate) mod delivered_toml_fragment; +pub use self::delivered_toml_fragment::DeliveredTomlFragment; + // Cloud Tasks pub(crate) mod code_task_details_response; pub use self::code_task_details_response::CodeTaskDetailsResponse; diff --git a/codex-rs/codex-client/src/custom_ca.rs b/codex-rs/codex-client/src/custom_ca.rs index 7a8b2f27bd..1a211beedf 100644 --- a/codex-rs/codex-client/src/custom_ca.rs +++ b/codex-rs/codex-client/src/custom_ca.rs @@ -477,15 +477,13 @@ impl ConfiguredCaBundle { })?; certificates.push(CertificateDer::from(cert_der.to_vec())); } - SectionKind::Crl => { - if !logged_crl_presence { - info!( - source_env = self.source_env, - ca_path = %self.path.display(), - "ignoring X509 CRL entries found in custom CA bundle" - ); - logged_crl_presence = true; - } + SectionKind::Crl if !logged_crl_presence => { + info!( + source_env = self.source_env, + ca_path = %self.path.display(), + "ignoring X509 CRL entries found in custom CA bundle" + ); + logged_crl_presence = true; } _ => {} } diff --git a/codex-rs/codex-mcp/src/codex_apps.rs b/codex-rs/codex-mcp/src/codex_apps.rs index eb88ffa6b1..3196a143e8 100644 --- a/codex-rs/codex-mcp/src/codex_apps.rs +++ b/codex-rs/codex-mcp/src/codex_apps.rs @@ -12,7 +12,9 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::runtime::emit_duration; use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC; use crate::tools::ToolInfo; +use anyhow::Context; use codex_login::CodexAuth; +use codex_protocol::mcp::McpServerInfo; use codex_utils_plugins::mcp_connector::is_connector_id_allowed; use codex_utils_plugins::mcp_connector::sanitize_name; use serde::Deserialize; @@ -20,8 +22,6 @@ use serde::Serialize; use sha1::Digest; use sha1::Sha1; -pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3; - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CodexAppsToolsCacheKey { pub(crate) account_id: Option, @@ -44,11 +44,19 @@ pub(crate) struct CodexAppsToolsCacheContext { } impl CodexAppsToolsCacheContext { - pub(crate) fn cache_path(&self) -> PathBuf { + pub(crate) fn tools_cache_path(&self) -> PathBuf { + self.cache_path_in(CODEX_APPS_TOOLS_CACHE_DIR) + } + + pub(crate) fn server_info_cache_path(&self) -> PathBuf { + self.cache_path_in(CODEX_APPS_SERVER_INFO_CACHE_DIR) + } + + fn cache_path_in(&self, cache_dir: &str) -> PathBuf { let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default(); let user_key_hash = sha1_hex(&user_key_json); self.codex_home - .join(CODEX_APPS_TOOLS_CACHE_DIR) + .join(cache_dir) .join(format!("{user_key_hash}.json")) } } @@ -136,6 +144,7 @@ pub(crate) fn normalize_codex_apps_callable_namespace( pub(crate) fn write_cached_codex_apps_tools_if_needed( server_name: &str, cache_context: Option<&CodexAppsToolsCacheContext>, + server_info: &McpServerInfo, tools: &[ToolInfo], ) { if server_name != CODEX_APPS_MCP_SERVER_NAME { @@ -145,6 +154,9 @@ pub(crate) fn write_cached_codex_apps_tools_if_needed( if let Some(cache_context) = cache_context { let cache_write_start = Instant::now(); write_cached_codex_apps_tools(cache_context, tools); + if let Err(err) = write_cached_codex_apps_server_info(cache_context, server_info) { + tracing::warn!("failed to write Codex Apps server info cache: {err:#}"); + } emit_duration( MCP_TOOLS_CACHE_WRITE_DURATION_METRIC, cache_write_start.elapsed(), @@ -169,6 +181,17 @@ pub(crate) fn load_startup_cached_codex_apps_tools_snapshot( } } +pub(crate) fn load_startup_cached_codex_apps_server_info( + server_name: &str, + cache_context: Option<&CodexAppsToolsCacheContext>, +) -> Option { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return None; + } + + load_cached_codex_apps_server_info(cache_context?) +} + #[cfg(test)] pub(crate) fn read_cached_codex_apps_tools( cache_context: &CodexAppsToolsCacheContext, @@ -182,7 +205,7 @@ pub(crate) fn read_cached_codex_apps_tools( pub(crate) fn load_cached_codex_apps_tools( cache_context: &CodexAppsToolsCacheContext, ) -> CachedCodexAppsToolsLoad { - let cache_path = cache_context.cache_path(); + let cache_path = cache_context.tools_cache_path(); let bytes = match std::fs::read(cache_path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { @@ -204,7 +227,7 @@ pub(crate) fn write_cached_codex_apps_tools( cache_context: &CodexAppsToolsCacheContext, tools: &[ToolInfo], ) { - let cache_path = cache_context.cache_path(); + let cache_path = cache_context.tools_cache_path(); if let Some(parent) = cache_path.parent() && std::fs::create_dir_all(parent).is_err() { @@ -220,6 +243,42 @@ pub(crate) fn write_cached_codex_apps_tools( let _ = std::fs::write(cache_path, bytes); } +pub(crate) fn load_cached_codex_apps_server_info( + cache_context: &CodexAppsToolsCacheContext, +) -> Option { + let bytes = std::fs::read(cache_context.server_info_cache_path()).ok()?; + let cache: CodexAppsServerInfoDiskCache = serde_json::from_slice(&bytes).ok()?; + (cache.schema_version == CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION) + .then_some(cache.server_info) +} + +fn write_cached_codex_apps_server_info( + cache_context: &CodexAppsToolsCacheContext, + server_info: &McpServerInfo, +) -> anyhow::Result<()> { + let cache_path = cache_context.server_info_cache_path(); + if let Some(parent) = cache_path.parent() { + std::fs::create_dir_all(parent).with_context(|| { + format!( + "failed to create Codex Apps server info cache directory `{}`", + parent.display() + ) + })?; + } + let bytes = serde_json::to_vec_pretty(&CodexAppsServerInfoDiskCache { + schema_version: CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION, + server_info: server_info.clone(), + }) + .context("failed to serialize Codex Apps server info cache")?; + std::fs::write(&cache_path, bytes).with_context(|| { + format!( + "failed to write Codex Apps server info cache `{}`", + cache_path.display() + ) + })?; + Ok(()) +} + pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec) -> Vec { tools .into_iter() @@ -237,7 +296,17 @@ struct CodexAppsToolsDiskCache { tools: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CodexAppsServerInfoDiskCache { + schema_version: u8, + server_info: McpServerInfo, +} + const CODEX_APPS_TOOLS_CACHE_DIR: &str = "cache/codex_apps_tools"; +pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3; + +const CODEX_APPS_SERVER_INFO_CACHE_DIR: &str = "cache/codex_apps_server_info"; +const CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION: u8 = 1; fn sha1_hex(s: &str) -> String { let mut hasher = Sha1::new(); diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 91ed8b1b64..d15a2e1500 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; +use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; @@ -44,6 +45,7 @@ use codex_config::McpServerTransportConfig; use codex_config::types::OAuthCredentialsStoreMode; use codex_login::CodexAuth; use codex_protocol::mcp::CallToolResult; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; @@ -62,11 +64,43 @@ use rmcp::model::ReadResourceResult; use rmcp::model::RequestId; use rmcp::model::Resource; use rmcp::model::ResourceTemplate; +use serde_json::Value as JsonValue; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; +use tracing::Instrument; use tracing::instrument; +use tracing::trace; +use tracing::trace_span; use tracing::warn; +const MCP_UI_META_KEY: &str = "ui"; +const MCP_UI_VISIBILITY_META_KEY: &str = "visibility"; +const MCP_UI_MODEL_VISIBILITY: &str = "model"; + +/// Returns whether a tool may be included in model-facing tool declarations. +/// +/// Tools without visibility metadata remain visible. +/// Tools with visibility metadata are hidden unless they explicitly include `model`. +/// +/// +pub fn tool_is_model_visible(tool: &ToolInfo) -> bool { + let Some(visibility) = tool + .tool + .meta + .as_deref() + .and_then(|meta| meta.get(MCP_UI_META_KEY)) + .and_then(JsonValue::as_object) + .and_then(|ui| ui.get(MCP_UI_VISIBILITY_META_KEY)) + .and_then(JsonValue::as_array) + else { + return true; + }; + + visibility + .iter() + .any(|target| target.as_str() == Some(MCP_UI_MODEL_VISIBILITY)) +} + /// A thin wrapper around a set of running [`RmcpClient`] instances. pub struct McpConnectionManager { clients: HashMap, @@ -378,13 +412,37 @@ impl McpConnectionManager { } /// Returns all tools with model-visible names normalized. - #[instrument(level = "trace", skip_all)] + #[instrument(level = "trace", skip_all, fields(mcp_server_count = self.clients.len()))] pub async fn list_all_tools(&self) -> Vec { let mut tools = Vec::new(); - for managed_client in self.clients.values() { - let Some(server_tools) = managed_client.listed_tools().await else { + for (server_name, managed_client) in &self.clients { + let has_cached_tool_info_snapshot = managed_client.cached_tool_info_snapshot.is_some(); + let startup_complete = managed_client + .startup_complete + .load(std::sync::atomic::Ordering::Acquire); + trace!( + server_name = %server_name, + has_cached_tool_info_snapshot, + startup_complete, + "waiting for MCP server tools while building tool list" + ); + let Some(server_tools) = managed_client + .listed_tools() + .instrument(trace_span!( + "list_tools_for_server", + server_name = %server_name, + has_cached_tool_info_snapshot, + startup_complete + )) + .await + else { continue; }; + trace!( + server_name = %server_name, + tool_count = server_tools.len(), + "listed MCP server tools while building tool list" + ); tools.extend( server_tools .into_iter() @@ -394,6 +452,31 @@ impl McpConnectionManager { normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names) } + /// Returns presentation metadata without waiting for uncached clients still initializing. + /// Cached values will be used if available and the server is still starting up. + pub async fn list_available_server_infos(&self) -> HashMap { + let mut server_infos = HashMap::new(); + for (server_name, client) in &self.clients { + if !client.startup_complete.load(Ordering::Acquire) { + if let Some(server_info) = client.cached_server_info.clone() { + server_infos.insert(server_name.clone(), server_info); + } + continue; + } + match client.client().await { + Ok(managed_client) => { + server_infos.insert(server_name.clone(), managed_client.server_info); + } + Err(_) => { + if let Some(server_info) = client.cached_server_info.clone() { + server_infos.insert(server_name.clone(), server_info); + } + } + } + } + server_infos + } + /// Force-refresh codex apps tools by bypassing the in-process cache. /// /// On success, the refreshed tools replace the cache contents and the @@ -429,6 +512,7 @@ impl McpConnectionManager { write_cached_codex_apps_tools_if_needed( CODEX_APPS_MCP_SERVER_NAME, managed_client.codex_apps_tools_cache_context.as_ref(), + &managed_client.server_info, &tools, ); emit_duration( @@ -483,9 +567,8 @@ impl McpConnectionManager { let mut cursor: Option = None; loop { - let params = cursor.as_ref().map(|next| PaginatedRequestParams { - meta: None, - cursor: Some(next.clone()), + let params = cursor.as_ref().map(|next| { + PaginatedRequestParams::default().with_cursor(Some(next.clone())) }); let response = match client.list_resources(params, timeout).await { Ok(result) => result, @@ -549,9 +632,8 @@ impl McpConnectionManager { let mut cursor: Option = None; loop { - let params = cursor.as_ref().map(|next| PaginatedRequestParams { - meta: None, - cursor: Some(next.clone()), + let params = cursor.as_ref().map(|next| { + PaginatedRequestParams::default().with_cursor(Some(next.clone())) }); let response = match client.list_resource_templates(params, timeout).await { Ok(result) => result, diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 86eb703e7a..60a0026eeb 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1,9 +1,11 @@ use super::*; use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION; use crate::codex_apps::CodexAppsToolsCacheContext; +use crate::codex_apps::load_startup_cached_codex_apps_server_info; use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot; use crate::codex_apps::read_cached_codex_apps_tools; use crate::codex_apps::write_cached_codex_apps_tools; +use crate::codex_apps::write_cached_codex_apps_tools_if_needed; use crate::declared_openai_file_input_param_names; use crate::elicitation::ElicitationRequestManager; use crate::elicitation::elicitation_is_rejected_by_policy; @@ -20,6 +22,7 @@ use codex_config::Constrained; use codex_config::McpServerConfig; use codex_exec_server::EnvironmentManager; use codex_protocol::ToolName; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::McpAuthStatus; @@ -44,17 +47,11 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { callable_name: tool_name.to_string(), callable_namespace: server_name.to_string(), namespace_description: None, - tool: Tool { - name: tool_name.to_string().into(), - title: None, - description: Some(format!("Test tool: {tool_name}").into()), - input_schema: Arc::new(JsonObject::default()), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: None, - }, + tool: Tool::new( + tool_name.to_string(), + format!("Test tool: {tool_name}"), + Arc::new(JsonObject::default()), + ), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), @@ -88,6 +85,17 @@ fn create_codex_apps_tools_cache_context( } } +fn create_test_server_info(title: &str) -> McpServerInfo { + McpServerInfo { + name: "codex-apps".to_string(), + title: Some(title.to_string()), + version: "1.0.0".to_string(), + description: None, + icons: None, + website_url: None, + } +} + fn model_tool_names(tools: &[ToolInfo]) -> HashSet { tools .iter() @@ -588,8 +596,8 @@ fn codex_apps_tools_cache_is_scoped_per_user() { assert_eq!(read_user_1[0].callable_name, "one"); assert_eq!(read_user_2[0].callable_name, "two"); assert_ne!( - cache_context_user_1.cache_path(), - cache_context_user_2.cache_path(), + cache_context_user_1.tools_cache_path(), + cache_context_user_2.tools_cache_path(), "each user should get an isolated cache file" ); } @@ -633,7 +641,7 @@ fn codex_apps_tools_cache_is_ignored_when_schema_version_mismatches() { Some("account-one"), Some("user-one"), ); - let cache_path = cache_context.cache_path(); + let cache_path = cache_context.tools_cache_path(); if let Some(parent) = cache_path.parent() { std::fs::create_dir_all(parent).expect("create parent"); } @@ -655,7 +663,7 @@ fn codex_apps_tools_cache_is_ignored_when_json_is_invalid() { Some("account-one"), Some("user-one"), ); - let cache_path = cache_context.cache_path(); + let cache_path = cache_context.tools_cache_path(); if let Some(parent) = cache_path.parent() { std::fs::create_dir_all(parent).expect("create parent"); } @@ -676,21 +684,112 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() { CODEX_APPS_MCP_SERVER_NAME, "calendar_search", )]; - write_cached_codex_apps_tools(&cache_context, &cached_tools); + let server_info = create_test_server_info("Codex Apps"); + write_cached_codex_apps_tools_if_needed( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + &server_info, + &cached_tools, + ); - let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot( + let startup_tools = load_startup_cached_codex_apps_tools_snapshot( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + ) + .expect("expected startup snapshot to load from cache"); + let cached_server_info = load_startup_cached_codex_apps_server_info( CODEX_APPS_MCP_SERVER_NAME, Some(&cache_context), ); - let startup_tools = startup_snapshot.expect("expected startup snapshot to load from cache"); assert_eq!(startup_tools.len(), 1); assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME); assert_eq!(startup_tools[0].callable_name, "calendar_search"); + assert_eq!(cached_server_info, Some(server_info)); +} + +#[test] +fn startup_cached_codex_apps_tools_loads_without_server_info_cache() { + let codex_home = tempdir().expect("tempdir"); + let cache_context = create_codex_apps_tools_cache_context( + codex_home.path().to_path_buf(), + Some("account-one"), + Some("user-one"), + ); + let cache_path = cache_context.tools_cache_path(); + if let Some(parent) = cache_path.parent() { + std::fs::create_dir_all(parent).expect("create parent"); + } + let bytes = serde_json::to_vec_pretty(&serde_json::json!({ + "schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION, + "tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")], + })) + .expect("serialize"); + std::fs::write(cache_path, bytes).expect("write"); + + let startup_tools = load_startup_cached_codex_apps_tools_snapshot( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + ) + .expect("legacy startup snapshot should remain available"); + let cached_server_info = load_startup_cached_codex_apps_server_info( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + ); + + assert_eq!(startup_tools.len(), 1); + assert_eq!(startup_tools[0].callable_name, "calendar_search"); + assert_eq!(cached_server_info, None); +} + +#[test] +fn codex_apps_server_info_cache_survives_legacy_tools_cache_write() { + let codex_home = tempdir().expect("tempdir"); + let cache_context = create_codex_apps_tools_cache_context( + codex_home.path().to_path_buf(), + Some("account-one"), + Some("user-one"), + ); + let server_info = create_test_server_info("Codex Apps"); + write_cached_codex_apps_tools_if_needed( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + &server_info, + &[create_test_tool( + CODEX_APPS_MCP_SERVER_NAME, + "calendar_search", + )], + ); + + let cache_path = cache_context.tools_cache_path(); + if let Some(parent) = cache_path.parent() { + std::fs::create_dir_all(parent).expect("create parent"); + } + let bytes = serde_json::to_vec_pretty(&serde_json::json!({ + "schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION - 1, + "tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")], + })) + .expect("serialize"); + std::fs::write(cache_path, bytes).expect("write legacy tools cache"); + + assert_eq!( + load_startup_cached_codex_apps_server_info( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + ), + Some(server_info) + ); + assert!( + load_startup_cached_codex_apps_tools_snapshot( + CODEX_APPS_MCP_SERVER_NAME, + Some(&cache_context), + ) + .is_none() + ); } #[tokio::test] -async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() { +async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending() { let startup_tools = vec![create_test_tool( CODEX_APPS_MCP_SERVER_NAME, "calendar_create_event", @@ -709,7 +808,8 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() { CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: Some(startup_tools), + cached_tool_info_snapshot: Some(startup_tools), + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -728,6 +828,43 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() { assert_eq!(tool.callable_name, "calendar_create_event"); } +#[tokio::test] +async fn list_available_server_infos_uses_cache_while_client_is_pending() { + let pending_client = futures::future::pending::>() + .boxed() + .shared(); + let approval_policy = Constrained::allow_any(AskForApproval::OnFailure); + let permission_profile = Constrained::allow_any(PermissionProfile::default()); + let mut manager = McpConnectionManager::new_uninitialized( + &approval_policy, + &permission_profile, + /*prefix_mcp_tool_names*/ true, + ); + let server_info = create_test_server_info("Codex Apps"); + manager.clients.insert( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + AsyncManagedClient { + client: pending_client, + cached_tool_info_snapshot: Some(Vec::new()), + cached_server_info: Some(server_info.clone()), + startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), + tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), + cancel_token: CancellationToken::new(), + }, + ); + + let timeout_result = tokio::time::timeout( + Duration::from_millis(10), + manager.list_available_server_infos(), + ) + .await; + let server_infos = timeout_result.expect("server info lookup should not block on startup"); + assert_eq!( + server_infos.get(CODEX_APPS_MCP_SERVER_NAME), + Some(&server_info) + ); +} + #[tokio::test] async fn list_all_tools_accepts_canonical_namespaced_tool_names() { let startup_tools = vec![create_test_tool("rmcp", "echo")]; @@ -745,7 +882,8 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() { "rmcp".to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: Some(startup_tools), + cached_tool_info_snapshot: Some(startup_tools), + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -787,7 +925,8 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() { "rmcp".to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: Some(startup_tools), + cached_tool_info_snapshot: Some(startup_tools), + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -813,7 +952,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() { } #[tokio::test] -async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot() { +async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_snapshot() { let pending_client = futures::future::pending::>() .boxed() .shared(); @@ -828,7 +967,8 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot( CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: None, + cached_tool_info_snapshot: None, + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -841,7 +981,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot( } #[tokio::test] -async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty() { +async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() { let pending_client = futures::future::pending::>() .boxed() .shared(); @@ -856,7 +996,8 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty( CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: Some(Vec::new()), + cached_tool_info_snapshot: Some(Vec::new()), + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -870,11 +1011,12 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty( } #[tokio::test] -async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() { +async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails() { let startup_tools = vec![create_test_tool( CODEX_APPS_MCP_SERVER_NAME, "calendar_create_event", )]; + let server_info = create_test_server_info("Codex Apps"); let failed_client = futures::future::ready::>(Err( StartupOutcomeError::Failed { error: "startup failed".to_string(), @@ -894,7 +1036,8 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() { CODEX_APPS_MCP_SERVER_NAME.to_string(), AsyncManagedClient { client: failed_client, - startup_snapshot: Some(startup_tools), + cached_tool_info_snapshot: Some(startup_tools), + cached_server_info: Some(server_info.clone()), startup_complete, tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), @@ -911,6 +1054,13 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() { .expect("tool from startup cache"); assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME); assert_eq!(tool.callable_name, "calendar_create_event"); + assert_eq!( + manager + .list_available_server_infos() + .await + .get(CODEX_APPS_MCP_SERVER_NAME), + Some(&server_info) + ); } #[tokio::test] @@ -941,7 +1091,8 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() { server_name.to_string(), AsyncManagedClient { client: pending_client, - startup_snapshot: Some(startup_tools), + cached_tool_info_snapshot: Some(startup_tools), + cached_server_info: None, startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)), tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()), cancel_token: CancellationToken::new(), diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index 7b45bcc7a7..83419175d4 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -1,4 +1,5 @@ pub use connection_manager::McpConnectionManager; +pub use connection_manager::tool_is_model_visible; pub use elicitation::ElicitationReviewRequest; pub use elicitation::ElicitationReviewer; pub use elicitation::ElicitationReviewerHandle; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 407ece427e..51a6f18686 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -25,6 +25,7 @@ use codex_config::types::ApprovalsReviewer; use codex_config::types::OAuthCredentialsStoreMode; use codex_login::CodexAuth; use codex_plugin::PluginCapabilitySummary; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::mcp::Tool; @@ -300,13 +301,7 @@ pub async fn read_mcp_resource( .await; let result = manager - .read_resource( - server, - ReadResourceRequestParams { - meta: None, - uri: uri.to_string(), - }, - ) + .read_resource(server, ReadResourceRequestParams::new(uri)) .await; cancel_token.cancel(); result @@ -314,6 +309,7 @@ pub async fn read_mcp_resource( #[derive(Debug, Clone)] pub struct McpServerStatusSnapshot { + pub server_infos: HashMap, pub tools_by_server: HashMap>, pub resources: HashMap>, pub resource_templates: HashMap>, @@ -333,6 +329,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( let tool_plugin_provenance = tool_plugin_provenance(config); if mcp_servers.is_empty() { return McpServerStatusSnapshot { + server_infos: HashMap::new(), tools_by_server: HashMap::new(), resources: HashMap::new(), resource_templates: HashMap::new(), @@ -605,6 +602,7 @@ async fn collect_mcp_server_status_snapshot_from_manager( } }, ); + let server_infos = mcp_connection_manager.list_available_server_infos().await; let mut tools_by_server = HashMap::>::new(); for tool_info in tools { @@ -620,6 +618,7 @@ async fn collect_mcp_server_status_snapshot_from_manager( } McpServerStatusSnapshot { + server_infos, tools_by_server, resources: convert_mcp_resources(resources), resource_templates: convert_mcp_resource_templates(resource_templates), diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index c75895842b..78078720e7 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -20,6 +20,7 @@ use crate::codex_apps::CachedCodexAppsToolsLoad; use crate::codex_apps::CodexAppsToolsCacheContext; use crate::codex_apps::filter_disallowed_codex_apps_tools; use crate::codex_apps::load_cached_codex_apps_tools; +use crate::codex_apps::load_startup_cached_codex_apps_server_info; use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot; use crate::codex_apps::normalize_codex_apps_callable_name; use crate::codex_apps::normalize_codex_apps_callable_namespace; @@ -47,6 +48,7 @@ use codex_config::McpServerTransportConfig; use codex_config::types::OAuthCredentialsStoreMode; use codex_exec_server::HttpClient; use codex_exec_server::ReqwestHttpClient; +use codex_protocol::mcp::McpServerInfo; use codex_protocol::protocol::Event; use codex_rmcp_client::ExecutorStdioServerLauncher; use codex_rmcp_client::LocalStdioServerLauncher; @@ -85,6 +87,7 @@ const UNTRUSTED_CONNECTOR_META_KEYS: &[&str] = &[ #[derive(Clone)] pub(crate) struct ManagedClient { pub(crate) client: Arc, + pub(crate) server_info: McpServerInfo, pub(crate) tools: Vec, pub(crate) tool_filter: ToolFilter, pub(crate) tool_timeout: Option, @@ -123,7 +126,8 @@ impl ManagedClient { #[derive(Clone)] pub(crate) struct AsyncManagedClient { pub(crate) client: Shared>>, - pub(crate) startup_snapshot: Option>, + pub(crate) cached_tool_info_snapshot: Option>, + pub(crate) cached_server_info: Option, pub(crate) startup_complete: Arc, pub(crate) tool_plugin_provenance: Arc, pub(crate) cancel_token: CancellationToken, @@ -150,11 +154,16 @@ impl AsyncManagedClient { .configured_config() .map(ToolFilter::from_config) .unwrap_or_default(); - let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot( + let cached_tool_info_snapshot = load_startup_cached_codex_apps_tools_snapshot( &server_name, codex_apps_tools_cache_context.as_ref(), - ) - .map(|tools| filter_tools(tools, &tool_filter)); + ); + let cached_tool_info_snapshot = + cached_tool_info_snapshot.map(|tools| filter_tools(tools, &tool_filter)); + let cached_server_info = load_startup_cached_codex_apps_server_info( + &server_name, + codex_apps_tools_cache_context.as_ref(), + ); let startup_tool_filter = tool_filter; let startup_complete = Arc::new(AtomicBool::new(false)); let startup_complete_for_fut = Arc::clone(&startup_complete); @@ -207,7 +216,7 @@ impl AsyncManagedClient { outcome }; let client = fut.boxed().shared(); - if startup_snapshot.is_some() { + if cached_tool_info_snapshot.is_some() { let startup_task = client.clone(); tokio::spawn(async move { let _ = startup_task.await; @@ -216,7 +225,8 @@ impl AsyncManagedClient { Self { client, - startup_snapshot, + cached_tool_info_snapshot, + cached_server_info, startup_complete, tool_plugin_provenance, cancel_token, @@ -238,9 +248,9 @@ impl AsyncManagedClient { } } - fn startup_snapshot_while_initializing(&self) -> Option> { + fn cached_tool_info_snapshot_while_initializing(&self) -> Option> { if !self.startup_complete.load(Ordering::Acquire) { - return self.startup_snapshot.clone(); + return self.cached_tool_info_snapshot.clone(); } None } @@ -298,12 +308,13 @@ impl AsyncManagedClient { }; // Keep cache payloads raw; plugin provenance is resolved per-session at read time. - let tools = if let Some(startup_tools) = self.startup_snapshot_while_initializing() { + let tools = if let Some(startup_tools) = self.cached_tool_info_snapshot_while_initializing() + { Some(startup_tools) } else { match self.client().await { Ok(client) => Some(client.listed_tools()), - Err(_) => self.startup_snapshot.clone(), + Err(_) => self.cached_tool_info_snapshot.clone(), } }; tools.map(annotate_tools) @@ -470,26 +481,13 @@ async fn start_server_task( codex_apps_tools_cache_context, client_elicitation_capability, } = params; - let params = InitializeRequestParams { - meta: None, - capabilities: ClientCapabilities { - experimental: None, - extensions: None, - roots: None, - sampling: None, - elicitation: Some(client_elicitation_capability), - tasks: None, - }, - client_info: Implementation { - name: "codex-mcp-client".to_owned(), - version: env!("CARGO_PKG_VERSION").to_owned(), - title: Some("Codex".into()), - description: None, - icons: None, - website_url: None, - }, - protocol_version: ProtocolVersion::V_2025_06_18, - }; + let mut capabilities = ClientCapabilities::default(); + capabilities.elicitation = Some(client_elicitation_capability); + let params = InitializeRequestParams::new( + capabilities, + Implementation::new("codex-mcp-client", env!("CARGO_PKG_VERSION")).with_title("Codex"), + ) + .with_protocol_version(ProtocolVersion::V_2025_06_18); let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event); @@ -519,9 +517,11 @@ async fn start_server_task( fetch_start.elapsed(), &[], ); + let server_info = mcp_server_info_from_implementation(initialize_result.server_info); write_cached_codex_apps_tools_if_needed( &server_name, codex_apps_tools_cache_context.as_ref(), + &server_info, &tools, ); if server_name == CODEX_APPS_MCP_SERVER_NAME { @@ -535,6 +535,7 @@ async fn start_server_task( let managed = ManagedClient { client: Arc::clone(&client), + server_info, tools, tool_timeout: Some(tool_timeout), tool_filter, @@ -546,6 +547,22 @@ async fn start_server_task( Ok(managed) } +fn mcp_server_info_from_implementation(server_info: Implementation) -> McpServerInfo { + McpServerInfo { + name: server_info.name, + title: server_info.title, + version: server_info.version, + description: server_info.description, + icons: server_info.icons.map(|icons| { + icons + .into_iter() + .filter_map(|icon| serde_json::to_value(icon).ok()) + .collect() + }), + website_url: server_info.website_url, + } +} + struct StartServerTaskParams { startup_timeout: Option, // TODO: cancel_token should handle this. tool_timeout: Duration, @@ -647,32 +664,27 @@ mod tests { use rmcp::model::Meta; fn tool_with_connector_meta() -> RmcpTool { - RmcpTool { - name: "capture_file_upload".to_string().into(), - title: None, - description: Some("test tool".to_string().into()), - input_schema: Arc::new(JsonObject::default()), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: Some(Meta( - serde_json::json!({ - "connector_id": "connector_gmail", - "connector_name": "Gmail", - "connector_display_name": "Gmail", - "connector_description": "Mail connector", - "connectorDescription": "Mail connector", - "connectorFutureField": "future connector metadata", - "CONNECTOR_UPPERCASE": "uppercase connector metadata", - "openai/fileParams": ["file"], - "custom": "kept" - }) - .as_object() - .expect("object") - .clone(), - )), - } + RmcpTool::new( + "capture_file_upload", + "test tool", + Arc::new(JsonObject::default()), + ) + .with_meta(Meta( + serde_json::json!({ + "connector_id": "connector_gmail", + "connector_name": "Gmail", + "connector_display_name": "Gmail", + "connector_description": "Mail connector", + "connectorDescription": "Mail connector", + "connectorFutureField": "future connector metadata", + "CONNECTOR_UPPERCASE": "uppercase connector metadata", + "openai/fileParams": ["file"], + "custom": "kept" + }) + .as_object() + .expect("object") + .clone(), + )) } #[test] diff --git a/codex-rs/config/src/cloud_config_layers.rs b/codex-rs/config/src/cloud_config_layers.rs new file mode 100644 index 0000000000..bdb11631e4 --- /dev/null +++ b/codex-rs/config/src/cloud_config_layers.rs @@ -0,0 +1,108 @@ +//! Conversion from cloud-delivered config TOML fragments into config stack layers. +//! +//! Backend fragments arrive in backend priority order. This module parses each +//! fragment, resolves relative path fields against the cloud config base +//! directory, and returns layers in `ConfigLayerStack` order. + +use crate::ConfigLayerEntry; +use crate::ConfigLayerSource; +use crate::TomlValue; +use crate::loader::resolve_relative_paths_in_config_toml; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::fmt; +use std::io; +use thiserror::Error; + +/// Config fragment delivered by the cloud config bundle. +/// +/// The bundle orders fragments from highest precedence to lowest precedence. +/// This module returns config layers in stack order, so callers can append the +/// result between system and user config without re-sorting. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CloudConfigFragment { + pub id: String, + pub name: String, + pub contents: String, +} + +impl CloudConfigFragment { + fn source_ref(&self) -> CloudConfigFragmentSource { + CloudConfigFragmentSource { + id: self.id.clone(), + name: self.name.clone(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CloudConfigFragmentSource { + pub id: String, + pub name: String, +} + +impl fmt::Display for CloudConfigFragmentSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({})", self.name, self.id) + } +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CloudConfigLayerError { + #[error("failed to parse cloud config fragment {fragment}: {message}")] + Parse { + fragment: CloudConfigFragmentSource, + message: String, + }, + #[error("invalid cloud config fragment {fragment}: {message}")] + Invalid { + fragment: CloudConfigFragmentSource, + message: String, + }, +} + +pub fn cloud_config_layers_from_fragments( + fragments: impl IntoIterator, + base_dir: &AbsolutePathBuf, +) -> Result, CloudConfigLayerError> { + let mut layers = Vec::new(); + for fragment in fragments { + let source_ref = fragment.source_ref(); + let raw_toml = fragment.contents; + let value: TomlValue = + toml::from_str(&raw_toml).map_err(|err| CloudConfigLayerError::Parse { + fragment: source_ref.clone(), + message: err.to_string(), + })?; + let resolved = + resolve_relative_paths_in_config_toml(value, base_dir.as_path()).map_err(|err| { + CloudConfigLayerError::Invalid { + fragment: source_ref.clone(), + message: err.to_string(), + } + })?; + layers.push(ConfigLayerEntry::new_with_raw_toml( + ConfigLayerSource::EnterpriseManaged { + id: fragment.id, + name: fragment.name, + }, + resolved, + raw_toml, + base_dir.clone(), + )); + } + + // Bundle fragments arrive highest-priority first, while ConfigLayerStack + // folds lowest-priority to highest-priority. + layers.reverse(); + Ok(layers) +} + +impl From for io::Error { + fn from(error: CloudConfigLayerError) -> Self { + io::Error::new(io::ErrorKind::InvalidData, error) + } +} + +#[cfg(test)] +#[path = "cloud_config_layers_tests.rs"] +mod tests; diff --git a/codex-rs/config/src/cloud_config_layers_tests.rs b/codex-rs/config/src/cloud_config_layers_tests.rs new file mode 100644 index 0000000000..c7f685fa67 --- /dev/null +++ b/codex-rs/config/src/cloud_config_layers_tests.rs @@ -0,0 +1,210 @@ +use super::*; +use crate::CONFIG_TOML_FILE; +use crate::ConfigLayerStack; +use crate::ConfigLayerStackOrdering; +use crate::ConfigRequirements; +use crate::ConfigRequirementsToml; +use crate::config_toml::ConfigToml; +use crate::first_layer_config_error_from_entries; +use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_absolute_path::test_support::PathBufExt; +use codex_utils_absolute_path::test_support::test_path_buf; +use pretty_assertions::assert_eq; +use std::path::Path; + +fn fragment(id: &str, name: &str, contents: &str) -> CloudConfigFragment { + CloudConfigFragment { + id: id.to_string(), + name: name.to_string(), + contents: contents.to_string(), + } +} + +fn toml(contents: &str) -> TomlValue { + toml::from_str(contents).expect("test TOML should parse") +} + +fn base_dir() -> AbsolutePathBuf { + test_path_buf("/var/lib/codex").abs() +} + +#[test] +fn layers_are_returned_in_stack_order() { + let base_dir = base_dir(); + let layers = cloud_config_layers_from_fragments( + vec![ + fragment("high", "High priority", "model = \"cloud-high\""), + fragment("low", "Low priority", "model_provider = \"cloud-low\""), + ], + &base_dir, + ) + .expect("cloud config layers should compose"); + + assert_eq!( + layers + .iter() + .map(|layer| layer.name.clone()) + .collect::>(), + vec![ + ConfigLayerSource::EnterpriseManaged { + id: "low".to_string(), + name: "Low priority".to_string(), + }, + ConfigLayerSource::EnterpriseManaged { + id: "high".to_string(), + name: "High priority".to_string(), + }, + ] + ); +} + +#[test] +fn enterprise_layers_precede_user_and_override_system() { + let base_dir = base_dir(); + let mut layers = vec![ConfigLayerEntry::new( + ConfigLayerSource::System { + file: test_path_buf("/etc/codex/config.toml").abs(), + }, + toml( + r#" +model = "system" +model_provider = "system" +review_model = "system-review" +"#, + ), + )]; + layers.extend( + cloud_config_layers_from_fragments( + vec![ + fragment("high", "High priority", "model_provider = \"cloud-high\""), + fragment("low", "Low priority", "review_model = \"cloud-low-review\""), + ], + &base_dir, + ) + .expect("cloud config layers should compose"), + ); + layers.push(ConfigLayerEntry::new( + ConfigLayerSource::User { + file: test_path_buf("/home/alice/.codex/config.toml").abs(), + profile: None, + }, + toml("model = \"user\""), + )); + + let stack = ConfigLayerStack::new( + layers, + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + ) + .expect("stack should be ordered"); + + assert_eq!( + stack + .get_layers( + ConfigLayerStackOrdering::LowestPrecedenceFirst, + /*include_disabled*/ false + ) + .iter() + .map(|layer| layer.name.clone()) + .collect::>(), + vec![ + ConfigLayerSource::System { + file: test_path_buf("/etc/codex/config.toml").abs(), + }, + ConfigLayerSource::EnterpriseManaged { + id: "low".to_string(), + name: "Low priority".to_string(), + }, + ConfigLayerSource::EnterpriseManaged { + id: "high".to_string(), + name: "High priority".to_string(), + }, + ConfigLayerSource::User { + file: test_path_buf("/home/alice/.codex/config.toml").abs(), + profile: None, + }, + ] + ); + assert_eq!( + stack.effective_config(), + toml( + r#" +model = "user" +model_provider = "cloud-high" +review_model = "cloud-low-review" +"#, + ) + ); +} + +#[test] +fn relative_absolute_path_fields_resolve_against_base_dir() { + let base_dir = base_dir(); + let layers = cloud_config_layers_from_fragments( + vec![fragment( + "cfg_123", + "Base policy", + "model_instructions_file = \"instructions.md\"", + )], + &base_dir, + ) + .expect("relative paths should match existing MDM semantics"); + + let path = layers[0] + .config + .get("model_instructions_file") + .and_then(TomlValue::as_str) + .expect("path should be present"); + let expected = + AbsolutePathBuf::resolve_path_against_base("instructions.md", base_dir.as_path()); + assert_eq!(path, expected.to_string_lossy()); +} + +#[test] +fn home_relative_path_fields_are_allowed_and_resolved() { + let base_dir = base_dir(); + let layers = cloud_config_layers_from_fragments( + vec![fragment( + "cfg_123", + "Base policy", + "model_instructions_file = \"~/instructions.md\"", + )], + &base_dir, + ) + .expect("home-relative paths should be accepted"); + + let path = layers[0] + .config + .get("model_instructions_file") + .and_then(TomlValue::as_str) + .expect("path should be present"); + let expected = + AbsolutePathBuf::resolve_path_against_base("~/instructions.md", base_dir.as_path()); + assert_eq!(path, expected.to_string_lossy()); +} + +#[tokio::test] +async fn raw_toml_diagnostics_use_enterprise_layer_name() { + let base_dir = base_dir(); + let layers = cloud_config_layers_from_fragments( + vec![fragment( + "cfg_123", + "Base policy", + "model_instructions_file = \"instructions.md\"\nmodel = 1", + )], + &base_dir, + ) + .expect("cloud config layers should parse"); + + let error = first_layer_config_error_from_entries::(&layers, CONFIG_TOML_FILE) + .await + .expect("invalid raw TOML should produce a layer diagnostic"); + + assert_eq!( + error.path, + Path::new("enterprise-managed (Base policy, cfg_123)").to_path_buf() + ); + assert_eq!(error.range.start.line, 2); + assert_eq!(error.range.start.column, 9); + assert!(error.message.contains("invalid type: integer `1`")); +} diff --git a/codex-rs/config/src/config_layer_source.rs b/codex-rs/config/src/config_layer_source.rs new file mode 100644 index 0000000000..7bff779b19 --- /dev/null +++ b/codex-rs/config/src/config_layer_source.rs @@ -0,0 +1,31 @@ +use codex_app_server_protocol::ConfigLayerSource; + +pub fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String { + match source { + ConfigLayerSource::Mdm { domain, key } => { + format!("MDM ({domain}:{key})") + } + ConfigLayerSource::System { file } => { + format!("system ({})", file.as_path().display()) + } + ConfigLayerSource::EnterpriseManaged { id, name } => { + format!("enterprise-managed ({name}, {id})") + } + ConfigLayerSource::User { file, .. } => { + format!("user ({})", file.as_path().display()) + } + ConfigLayerSource::Project { dot_codex_folder } => { + format!( + "project ({}/{config_toml_file})", + dot_codex_folder.as_path().display() + ) + } + ConfigLayerSource::SessionFlags => "session-flags".to_string(), + ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => { + format!("legacy managed_config.toml ({})", file.as_path().display()) + } + ConfigLayerSource::LegacyManagedConfigTomlFromMdm => { + "legacy managed_config.toml (MDM)".to_string() + } + } +} diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 93d16d3ccf..86d64e4153 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -20,17 +20,66 @@ use crate::ConstraintError; use crate::ManagedHooksRequirementsToml; use crate::mcp_types::AppToolApproval; use crate::permissions_toml::PermissionProfileToml; +use crate::types::WindowsSandboxModeToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { Unknown, - MdmManagedPreferences { domain: String, key: String }, + MdmManagedPreferences { + domain: String, + key: String, + }, CloudRequirements, - SystemRequirementsToml { file: AbsolutePathBuf }, - LegacyManagedConfigTomlFromFile { file: AbsolutePathBuf }, + /// Multiple requirements layers contributed to the final value. Sources are + /// stored highest-priority first, matching the order surfaced in errors. + Composite { + sources: Vec, + }, + /// A backend-delivered enterprise-managed layer. `id` is the stable backend + /// identifier; `name` is the admin-facing display name. + EnterpriseManaged { + id: String, + name: String, + }, + SystemRequirementsToml { + file: AbsolutePathBuf, + }, + LegacyManagedConfigTomlFromFile { + file: AbsolutePathBuf, + }, LegacyManagedConfigTomlFromMdm, } +impl RequirementSource { + pub fn composite(sources: impl IntoIterator) -> Self { + let mut flattened = Vec::new(); + for source in sources { + source.append_to_composite(&mut flattened); + } + + match flattened.len() { + 0 => RequirementSource::Unknown, + 1 => flattened.remove(0), + _ => RequirementSource::Composite { sources: flattened }, + } + } + + fn append_to_composite(self, flattened: &mut Vec) { + match self { + RequirementSource::Composite { sources } => { + for source in sources { + source.append_to_composite(flattened); + } + } + source => { + if !flattened.contains(&source) { + flattened.push(source); + } + } + } + } +} + impl fmt::Display for RequirementSource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -41,6 +90,19 @@ impl fmt::Display for RequirementSource { RequirementSource::CloudRequirements => { write!(f, "cloud requirements") } + RequirementSource::Composite { sources } => { + write!(f, "requirements layers: ")?; + for (index, source) in sources.iter().enumerate() { + if index > 0 { + write!(f, ", ")?; + } + write!(f, "{source}")?; + } + Ok(()) + } + RequirementSource::EnterpriseManaged { id, name } => { + write!(f, "enterprise-managed requirements {name} ({id})") + } RequirementSource::SystemRequirementsToml { file } => { write!(f, "{}", file.as_path().display()) } @@ -87,6 +149,7 @@ pub struct ConfigRequirements { pub approval_policy: ConstrainedWithSource, pub approvals_reviewer: ConstrainedWithSource, pub permission_profile: ConstrainedWithSource, + pub windows_sandbox_mode: ConstrainedWithSource>, pub web_search_mode: ConstrainedWithSource, pub allow_managed_hooks_only: Option>, pub allow_appshots: Option>, @@ -120,6 +183,10 @@ impl Default for ConfigRequirements { Constrained::allow_any(PermissionProfile::read_only()), /*source*/ None, ), + windows_sandbox_mode: ConstrainedWithSource::new( + Constrained::allow_any(/*initial_value*/ None), + /*source*/ None, + ), web_search_mode: ConstrainedWithSource::new( Constrained::allow_any(WebSearchMode::Cached), /*source*/ None, @@ -245,14 +312,14 @@ impl NetworkUnixSocketPermissionsToml { #[serde(rename_all = "lowercase")] pub enum NetworkUnixSocketPermissionToml { Allow, - None, + Deny, } impl std::fmt::Display for NetworkUnixSocketPermissionToml { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let permission = match self { Self::Allow => "allow", - Self::None => "none", + Self::Deny => "deny", }; f.write_str(permission) } @@ -648,6 +715,17 @@ impl ComputerUseRequirementsToml { } } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct WindowsRequirementsToml { + pub allowed_sandbox_implementations: Option>, +} + +impl WindowsRequirementsToml { + pub fn is_empty(&self) -> bool { + self.allowed_sandbox_implementations.is_none() + } +} + #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct FeatureRequirementsToml { #[serde(flatten)] @@ -754,6 +832,7 @@ pub struct ConfigRequirementsToml { pub allow_managed_hooks_only: Option, pub allow_appshots: Option, pub computer_use: Option, + pub windows: Option, #[serde(rename = "features", alias = "feature_requirements")] pub feature_requirements: Option, pub hooks: Option, @@ -806,6 +885,7 @@ pub struct ConfigRequirementsWithSources { pub allow_managed_hooks_only: Option>, pub allow_appshots: Option>, pub computer_use: Option>, + pub windows: Option>, pub feature_requirements: Option>, pub hooks: Option>, pub mcp_servers: Option>>, @@ -846,6 +926,7 @@ impl ConfigRequirementsWithSources { allow_managed_hooks_only: _, allow_appshots: _, computer_use: _, + windows: _, feature_requirements: _, hooks: _, mcp_servers: _, @@ -879,6 +960,7 @@ impl ConfigRequirementsWithSources { allow_managed_hooks_only, allow_appshots, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -910,6 +992,7 @@ impl ConfigRequirementsWithSources { allow_managed_hooks_only, allow_appshots, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -931,6 +1014,7 @@ impl ConfigRequirementsWithSources { allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), allow_appshots: allow_appshots.map(|sourced| sourced.value), computer_use: computer_use.map(|sourced| sourced.value), + windows: windows.map(|sourced| sourced.value), feature_requirements: feature_requirements.map(|sourced| sourced.value), hooks: hooks.map(|sourced| sourced.value), mcp_servers: mcp_servers.map(|sourced| sourced.value), @@ -960,7 +1044,7 @@ fn hostname_matches_any_pattern(hostname: &str, patterns: &[String]) -> bool { /// Currently, `external-sandbox` is not supported in config.toml, but it is /// supported through programmatic use. -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] pub enum SandboxModeRequirement { #[serde(rename = "read-only")] ReadOnly, @@ -1021,6 +1105,10 @@ impl ConfigRequirementsToml { .computer_use .as_ref() .is_none_or(ComputerUseRequirementsToml::is_empty) + && self + .windows + .as_ref() + .is_none_or(WindowsRequirementsToml::is_empty) && self .feature_requirements .as_ref() @@ -1065,6 +1153,7 @@ impl TryFrom for ConfigRequirements { allow_managed_hooks_only, allow_appshots, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -1174,6 +1263,44 @@ impl TryFrom for ConfigRequirements { /*source*/ None, ), }; + let windows_sandbox_mode = match windows { + Some(Sourced { + value: + WindowsRequirementsToml { + allowed_sandbox_implementations: Some(implementations), + }, + source: requirement_source, + }) => { + if implementations.is_empty() { + return Err(ConstraintError::empty_field( + "windows.allowed_sandbox_implementations", + )); + } + // Prefer elevated when both Windows sandbox implementations are allowed. + let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) { + WindowsSandboxModeToml::Elevated + } else { + WindowsSandboxModeToml::Unelevated + }; + + let requirement_source_for_error = requirement_source.clone(); + let constrained = + Constrained::new(Some(initial_value), move |candidate| match candidate { + Some(candidate) if implementations.contains(candidate) => Ok(()), + _ => Err(ConstraintError::InvalidValue { + field_name: "windows.sandbox", + candidate: format!("{candidate:?}"), + allowed: format!("{implementations:?}"), + requirement_source: requirement_source_for_error.clone(), + }), + })?; + ConstrainedWithSource::new(constrained, Some(requirement_source)) + } + Some(_) | None => ConstrainedWithSource::new( + Constrained::allow_any(/*initial_value*/ None), + /*source*/ None, + ), + }; let exec_policy = match rules { Some(Sourced { value, source }) => { let policy = value.to_requirements_policy().map_err(|err| { @@ -1299,6 +1426,7 @@ impl TryFrom for ConfigRequirements { approval_policy, approvals_reviewer, permission_profile, + windows_sandbox_mode, web_search_mode, allow_managed_hooks_only, allow_appshots, @@ -1363,6 +1491,41 @@ mod tests { )?) } + #[test] + fn composite_requirement_source_flattens_and_deduplicates_sources() { + let mdm_source = RequirementSource::MdmManagedPreferences { + domain: "com.openai.codex".to_string(), + key: "requirements_toml_base64".to_string(), + }; + let legacy_source = RequirementSource::LegacyManagedConfigTomlFromMdm; + + assert_eq!( + RequirementSource::composite([ + mdm_source.clone(), + RequirementSource::composite([legacy_source.clone(), mdm_source.clone()]), + ]), + RequirementSource::Composite { + sources: vec![mdm_source, legacy_source], + } + ); + } + + #[test] + fn composite_requirement_source_display_lists_sources_in_priority_order() { + let source = RequirementSource::composite([ + RequirementSource::MdmManagedPreferences { + domain: "com.openai.codex".to_string(), + key: "requirements_toml_base64".to_string(), + }, + RequirementSource::LegacyManagedConfigTomlFromMdm, + ]); + + assert_eq!( + source.to_string(), + "requirements layers: MDM com.openai.codex:requirements_toml_base64, MDM managed_config.toml (legacy)" + ); + } + fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources { let ConfigRequirementsToml { allowed_approval_policies, @@ -1374,6 +1537,7 @@ mod tests { allow_managed_hooks_only, allow_appshots, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -1401,6 +1565,7 @@ mod tests { allow_appshots: allow_appshots .map(|value| Sourced::new(value, RequirementSource::Unknown)), computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)), + windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)), feature_requirements: feature_requirements .map(|value| Sourced::new(value, RequirementSource::Unknown)), hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)), @@ -1581,6 +1746,7 @@ mod tests { allow_managed_hooks_only: Some(true), allow_appshots: Some(false), computer_use: Some(computer_use.clone()), + windows: None, feature_requirements: Some(feature_requirements.clone()), hooks: None, mcp_servers: None, @@ -1621,6 +1787,7 @@ mod tests { )), allow_appshots: Some(Sourced::new(/*value*/ false, enforce_source.clone(),)), computer_use: Some(Sourced::new(computer_use, enforce_source.clone())), + windows: None, feature_requirements: Some(Sourced::new( feature_requirements, enforce_source.clone(), @@ -1667,6 +1834,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1718,6 +1886,7 @@ mod tests { allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -2200,14 +2369,20 @@ allowed_approvals_reviewers = ["user"] } #[test] - fn constraint_error_includes_cloud_requirements_source() -> Result<()> { + fn constraint_error_includes_composite_requirement_source() -> Result<()> { let source: ConfigRequirementsToml = from_str( r#" allowed_approval_policies = ["on-request"] "#, )?; - let source_location = RequirementSource::CloudRequirements; + let source_location = RequirementSource::composite([ + RequirementSource::MdmManagedPreferences { + domain: "com.openai.codex".to_string(), + key: "requirements_toml_base64".to_string(), + }, + RequirementSource::LegacyManagedConfigTomlFromMdm, + ]); let mut target = ConfigRequirementsWithSources::default(); target.merge_unset_fields(source_location.clone(), source); @@ -2357,6 +2532,71 @@ allowed_approvals_reviewers = ["user"] Ok(()) } + #[test] + fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = ["elevated"] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert_eq!( + requirements.windows_sandbox_mode.value(), + Some(WindowsSandboxModeToml::Elevated) + ); + assert!( + requirements + .windows_sandbox_mode + .can_set(&Some(WindowsSandboxModeToml::Elevated)) + .is_ok() + ); + assert!( + requirements + .windows_sandbox_mode + .can_set(&Some(WindowsSandboxModeToml::Unelevated)) + .is_err() + ); + assert!(requirements.windows_sandbox_mode.can_set(&None).is_err()); + + Ok(()) + } + + #[test] + fn empty_allowed_windows_sandbox_implementations_is_rejected() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = [] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + + assert_eq!( + ConfigRequirements::try_from(with_unknown_source(config)), + Err(ConstraintError::EmptyField { + field_name: "windows.allowed_sandbox_implementations".to_string(), + }) + ); + + Ok(()) + } + + #[test] + fn allowed_windows_sandbox_implementations_prefer_elevated_fallback() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = ["unelevated", "elevated"] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert_eq!( + requirements.windows_sandbox_mode.value(), + Some(WindowsSandboxModeToml::Elevated) + ); + + Ok(()) + } + #[test] fn deserialize_legacy_allowed_approvals_reviewer() -> Result<()> { let toml_str = r#" @@ -2868,6 +3108,7 @@ command = "python3 /enterprise/hooks/pre.py" [experimental_network.unix_sockets] "/tmp/example.sock" = "allow" + "/tmp/blocked.sock" = "deny" "#; let source = RequirementSource::CloudRequirements; @@ -2912,10 +3153,16 @@ command = "python3 /enterprise/hooks/pre.py" assert_eq!( sourced_network.value.unix_sockets.as_ref(), Some(&NetworkUnixSocketPermissionsToml { - entries: BTreeMap::from([( - "/tmp/example.sock".to_string(), - NetworkUnixSocketPermissionToml::Allow, - )]), + entries: BTreeMap::from([ + ( + "/tmp/blocked.sock".to_string(), + NetworkUnixSocketPermissionToml::Deny, + ), + ( + "/tmp/example.sock".to_string(), + NetworkUnixSocketPermissionToml::Allow, + ), + ]), }) ); assert_eq!(sourced_network.value.allow_local_binding, Some(false)); @@ -3053,7 +3300,7 @@ command = "python3 /enterprise/hooks/pre.py" ), ( "/tmp/ignored.sock".to_string(), - NetworkUnixSocketPermissionToml::None, + NetworkUnixSocketPermissionToml::Deny, ), ]), }; diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 20a0769d83..0765819fb7 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -87,6 +87,10 @@ const fn default_hide_agent_reasoning() -> Option { Some(false) } +const fn default_true() -> bool { + true +} + /// Backward-compatible shape for ChatGPT workspace login restrictions in config.toml. #[derive(Serialize, Debug, Clone, PartialEq, JsonSchema)] #[serde(untagged)] @@ -617,6 +621,14 @@ pub struct ToolsToml { deserialize_with = "deserialize_optional_web_search_tool_config" )] pub web_search: Option, + pub experimental_request_user_input: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ExperimentalRequestUserInput { + #[serde(default = "default_true")] + pub enabled: bool, } #[derive(Deserialize)] @@ -861,7 +873,7 @@ fn project_config_for_lookup_key( .iter() .filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key) .collect(); - normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches.sort_by_key(|(key, _)| *key); normalized_matches .first() .map(|(_, project_config)| (**project_config).clone()) diff --git a/codex-rs/config/src/diagnostics.rs b/codex-rs/config/src/diagnostics.rs index 3fc593dcdf..ca7df712d6 100644 --- a/codex-rs/config/src/diagnostics.rs +++ b/codex-rs/config/src/diagnostics.rs @@ -4,6 +4,7 @@ use crate::ConfigLayerEntry; use crate::ConfigLayerStack; use crate::ConfigLayerStackOrdering; +use crate::format_config_layer_source; use codex_app_server_protocol::ConfigLayerSource; use codex_utils_absolute_path::AbsolutePathBufGuard; use serde::de::DeserializeOwned; @@ -89,7 +90,6 @@ impl std::error::Error for ConfigLoadError { #[derive(Clone, Copy)] pub(crate) enum ConfigDiagnosticSource<'a> { Path(&'a Path), - #[cfg(any(target_os = "macos", test))] DisplayName(&'a str), } @@ -97,7 +97,6 @@ impl ConfigDiagnosticSource<'_> { pub(crate) fn to_path_buf(self) -> PathBuf { match self { ConfigDiagnosticSource::Path(path) => path.to_path_buf(), - #[cfg(any(target_os = "macos", test))] ConfigDiagnosticSource::DisplayName(name) => PathBuf::from(name), } } @@ -201,6 +200,27 @@ where I: IntoIterator, { for layer in layers { + if let Some(contents) = layer.raw_toml() { + let source_name = format_config_layer_source(&layer.name, config_toml_file); + let Some(base_dir) = layer.raw_toml_base_dir() else { + tracing::debug!( + "Skipping raw TOML diagnostics for {source_name} because it has no base directory" + ); + continue; + }; + // Match the base directory used when the raw non-file layer was + // parsed into the runtime layer so diagnostics resolve relative + // path fields with the same semantics. + let _absolute_path_base = AbsolutePathBufGuard::new(base_dir.as_path()); + if let Some(error) = config_error_from_typed_toml_for_source::( + ConfigDiagnosticSource::DisplayName(&source_name), + contents, + ) { + return Some(error); + } + continue; + } + let Some(path) = config_path_for_layer(layer, config_toml_file) else { continue; }; @@ -235,6 +255,7 @@ fn config_path_for_layer(layer: &ConfigLayerEntry, config_toml_file: &str) -> Op } ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.to_path_buf()), ConfigLayerSource::Mdm { .. } + | ConfigLayerSource::EnterpriseManaged { .. } | ConfigLayerSource::SessionFlags | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None, } diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 5ae2688036..cb3984729b 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -1,4 +1,6 @@ +mod cloud_config_layers; mod cloud_requirements; +mod config_layer_source; mod config_requirements; pub mod config_toml; mod constraint; @@ -18,6 +20,7 @@ mod plugin_edit; pub mod profile_toml; mod project_root_markers; mod requirements_exec_policy; +mod requirements_layers; pub mod schema; mod skills_config; mod state; @@ -28,6 +31,10 @@ pub mod types; pub const CONFIG_TOML_FILE: &str = "config.toml"; +pub use cloud_config_layers::CloudConfigFragment; +pub use cloud_config_layers::CloudConfigFragmentSource; +pub use cloud_config_layers::CloudConfigLayerError; +pub use cloud_config_layers::cloud_config_layers_from_fragments; pub use cloud_requirements::CloudRequirementsLoadError; pub use cloud_requirements::CloudRequirementsLoadErrorCode; pub use cloud_requirements::CloudRequirementsLoader; @@ -35,6 +42,7 @@ pub use codex_app_server_protocol::ConfigLayerSource; pub use codex_protocol::config_types::ProfileV2Name; pub use codex_protocol::config_types::ProfileV2NameParseError; pub use codex_utils_absolute_path::AbsolutePathBuf; +pub use config_layer_source::format_config_layer_source; pub use config_requirements::AppRequirementToml; pub use config_requirements::AppToolRequirementToml; pub use config_requirements::AppToolsRequirementsToml; @@ -62,6 +70,7 @@ pub use config_requirements::ResidencyRequirement; pub use config_requirements::SandboxModeRequirement; pub use config_requirements::Sourced; pub use config_requirements::WebSearchModeRequirement; +pub use config_requirements::WindowsRequirementsToml; pub use config_requirements::sandbox_mode_requirement_for_permission_profile; pub use constraint::Constrained; pub use constraint::ConstraintError; @@ -116,6 +125,8 @@ pub use requirements_exec_policy::RequirementsExecPolicyParseError; pub use requirements_exec_policy::RequirementsExecPolicyPatternTokenToml; pub use requirements_exec_policy::RequirementsExecPolicyPrefixRuleToml; pub use requirements_exec_policy::RequirementsExecPolicyToml; +pub use requirements_layers::RequirementsLayerEntry; +pub use requirements_layers::compose_requirements; pub use skills_config::BundledSkillsConfig; pub use skills_config::SkillConfig; pub use skills_config::SkillsConfig; diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index e8972ce5ad..a657af0f36 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -366,13 +366,18 @@ pub async fn load_config_layers_state( // paths, starting with `./`, but a path starting with `~/` _is_ a // supported use case. Because resolve_relative_paths_in_config_toml() // relies on AbsolutePathBufGuard to resolve `~/`, we must supply a - // value for base_dir, so codex_home is as good a value as any. - let managed_config = - resolve_relative_paths_in_config_toml(config.managed_config, codex_home)?; + // value for base_dir. Preserve that same base on the layer so later + // raw-TOML diagnostics parse with the same path semantics. + let raw_toml_base_dir = AbsolutePathBuf::from_absolute_path(codex_home)?; + let managed_config = resolve_relative_paths_in_config_toml( + config.managed_config, + raw_toml_base_dir.as_path(), + )?; layers.push(ConfigLayerEntry::new_with_raw_toml( ConfigLayerSource::LegacyManagedConfigTomlFromMdm, managed_config, config.raw_toml, + raw_toml_base_dir, )); } @@ -1006,7 +1011,7 @@ fn project_trust_for_lookup_key( .iter() .filter(|(key, _)| normalize_project_trust_lookup_key((*key).clone()) == lookup_key) .collect(); - normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches.sort_by_key(|(key, _)| *key); normalized_matches .first() .map(|(key, trust_level)| ((**key).clone(), **trust_level)) diff --git a/codex-rs/config/src/loader/tests.rs b/codex-rs/config/src/loader/tests.rs index 2c87e1381d..9d4511a8a6 100644 --- a/codex-rs/config/src/loader/tests.rs +++ b/codex-rs/config/src/loader/tests.rs @@ -8,12 +8,33 @@ use codex_file_system::FileSystemSandboxContext; use codex_file_system::ReadDirectoryEntry; use codex_file_system::RemoveOptions; use pretty_assertions::assert_eq; +use std::path::Path; use tempfile::tempdir; struct TestFileSystem; #[async_trait] impl ExecutorFileSystem for TestFileSystem { + async fn canonicalize( + &self, + path: &AbsolutePathBuf, + _sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult { + path.canonicalize() + } + + async fn join( + &self, + base_path: &AbsolutePathBuf, + path: &Path, + ) -> FileSystemResult { + Ok(base_path.join(path)) + } + + async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult> { + Ok(path.parent()) + } + async fn read_file( &self, path: &AbsolutePathBuf, diff --git a/codex-rs/config/src/mcp_edit.rs b/codex-rs/config/src/mcp_edit.rs index 7e062e2998..9f007de113 100644 --- a/codex-rs/config/src/mcp_edit.rs +++ b/codex-rs/config/src/mcp_edit.rs @@ -230,7 +230,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { let mut tools = TomlTable::new(); tools.set_implicit(false); let mut tool_entries: Vec<_> = config.tools.iter().collect(); - tool_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + tool_entries.sort_by_key(|(name, _)| *name); for (name, tool_config) in tool_entries { let mut tool_entry = TomlTable::new(); tool_entry.set_implicit(false); @@ -280,7 +280,7 @@ where I: IntoIterator, { let mut entries: Vec<_> = pairs.into_iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut table = TomlTable::new(); table.set_implicit(false); for (key, value_str) in entries { diff --git a/codex-rs/config/src/permissions_toml.rs b/codex-rs/config/src/permissions_toml.rs index a4ef5d266e..0baec2a5cc 100644 --- a/codex-rs/config/src/permissions_toml.rs +++ b/codex-rs/config/src/permissions_toml.rs @@ -327,14 +327,14 @@ impl NetworkUnixSocketPermissionsToml { #[serde(rename_all = "lowercase")] pub enum NetworkUnixSocketPermissionToml { Allow, - None, + Deny, } impl std::fmt::Display for NetworkUnixSocketPermissionToml { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let permission = match self { Self::Allow => "allow", - Self::None => "none", + Self::Deny => "deny", }; f.write_str(permission) } @@ -547,7 +547,7 @@ impl NetworkToml { NetworkUnixSocketPermissionToml::Allow => { ProxyNetworkUnixSocketPermission::Allow } - NetworkUnixSocketPermissionToml::None => ProxyNetworkUnixSocketPermission::None, + NetworkUnixSocketPermissionToml::Deny => ProxyNetworkUnixSocketPermission::Deny, }; proxy_unix_sockets.entries.insert(path.clone(), permission); } diff --git a/codex-rs/config/src/requirements_layers/hooks.rs b/codex-rs/config/src/requirements_layers/hooks.rs new file mode 100644 index 0000000000..94aefd3285 --- /dev/null +++ b/codex-rs/config/src/requirements_layers/hooks.rs @@ -0,0 +1,237 @@ +//! Hook events are append-only across requirements layers. The managed hook +//! directory is different: only one directory is usable on a given platform, so +//! conflicting values for the active platform fail closed. The inactive platform +//! field is first-filled to allow the same layer stack to carry OS-specific +//! directories. + +use crate::HookEventsToml; +use crate::ManagedHooksRequirementsToml; +use crate::RequirementSource; +use crate::Sourced; +use std::collections::BTreeMap; +use std::path::PathBuf; + +use super::stack::composition_conflict; +use super::stack::merge_output_source; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub(super) enum HookDirectoryField { + #[default] + ManagedDir, + WindowsManagedDir, +} + +impl HookDirectoryField { + pub(super) fn current_platform() -> Self { + if cfg!(windows) { + Self::WindowsManagedDir + } else { + Self::ManagedDir + } + } + + fn field_name(self) -> &'static str { + match self { + Self::ManagedDir => "hooks.managed_dir", + Self::WindowsManagedDir => "hooks.windows_managed_dir", + } + } + + fn inactive(self) -> Self { + match self { + Self::ManagedDir => Self::WindowsManagedDir, + Self::WindowsManagedDir => Self::ManagedDir, + } + } +} + +pub(super) struct HookMergeState { + directory_field: HookDirectoryField, + dir_sources: BTreeMap, +} + +impl HookMergeState { + pub(super) fn new(directory_field: HookDirectoryField) -> Self { + Self { + directory_field, + dir_sources: BTreeMap::new(), + } + } + + pub(super) fn merge( + &mut self, + target: &mut Option>, + incoming: Option, + source: &RequirementSource, + ) -> Result<(), super::stack::RequirementsCompositionError> { + let Some(mut incoming) = incoming.filter(|value| !value.is_empty()) else { + return Ok(()); + }; + let Some(existing) = target.as_mut() else { + self.track_singleton_source( + HookDirectoryField::ManagedDir, + &incoming.managed_dir, + source, + ); + self.track_singleton_source( + HookDirectoryField::WindowsManagedDir, + &incoming.windows_managed_dir, + source, + ); + *target = Some(Sourced::new(incoming, source.clone())); + return Ok(()); + }; + + let active_field = self.directory_field; + let inactive_field = active_field.inactive(); + let incoming_active_dir = take_hook_dir(&mut incoming, active_field); + let incoming_inactive_dir = take_hook_dir(&mut incoming, inactive_field); + let mut changed = false; + changed |= self.merge_active_singleton( + active_field, + hook_dir_mut(&mut existing.value, active_field), + incoming_active_dir, + source, + )?; + changed |= self.fill_singleton( + inactive_field, + hook_dir_mut(&mut existing.value, inactive_field), + incoming_inactive_dir, + source, + ); + changed |= append_hook_events(&mut existing.value.hooks, incoming.hooks); + if changed { + merge_output_source(&mut existing.source, source); + } + Ok(()) + } + + fn track_singleton_source( + &mut self, + field: HookDirectoryField, + value: &Option, + source: &RequirementSource, + ) { + if value.is_some() { + self.dir_sources + .entry(field) + .or_insert_with(|| source.clone()); + } + } + + fn merge_active_singleton( + &mut self, + field: HookDirectoryField, + existing: &mut Option, + incoming: Option, + incoming_source: &RequirementSource, + ) -> Result { + let Some(incoming) = incoming else { + return Ok(false); + }; + + match existing { + Some(existing_value) if existing_value != &incoming => { + let existing_source = self + .dir_sources + .get(&field) + .cloned() + .unwrap_or_else(|| incoming_source.clone()); + Err(composition_conflict( + field.field_name().to_string(), + existing_source, + incoming_source.clone(), + format!( + "`{}` conflicts with `{}`", + existing_value.display(), + incoming.display() + ), + )) + } + Some(_) => Ok(false), + None => { + *existing = Some(incoming); + self.dir_sources + .entry(field) + .or_insert_with(|| incoming_source.clone()); + Ok(true) + } + } + } + + fn fill_singleton( + &mut self, + field: HookDirectoryField, + existing: &mut Option, + incoming: Option, + incoming_source: &RequirementSource, + ) -> bool { + if existing.is_none() + && let Some(incoming) = incoming + { + *existing = Some(incoming); + self.dir_sources + .entry(field) + .or_insert_with(|| incoming_source.clone()); + true + } else { + false + } + } +} + +fn take_hook_dir( + hooks: &mut ManagedHooksRequirementsToml, + field: HookDirectoryField, +) -> Option { + match field { + HookDirectoryField::ManagedDir => hooks.managed_dir.take(), + HookDirectoryField::WindowsManagedDir => hooks.windows_managed_dir.take(), + } +} + +fn hook_dir_mut( + hooks: &mut ManagedHooksRequirementsToml, + field: HookDirectoryField, +) -> &mut Option { + match field { + HookDirectoryField::ManagedDir => &mut hooks.managed_dir, + HookDirectoryField::WindowsManagedDir => &mut hooks.windows_managed_dir, + } +} + +fn append_hook_events(existing: &mut HookEventsToml, incoming: HookEventsToml) -> bool { + // Destructure without `..` so new hook events cannot be introduced without + // deciding whether requirements layer merging should append them. + let HookEventsToml { + pre_tool_use, + permission_request, + post_tool_use, + pre_compact, + post_compact, + session_start, + user_prompt_submit, + subagent_start, + subagent_stop, + stop, + } = incoming; + + let mut changed = false; + changed |= append_vec(&mut existing.pre_tool_use, pre_tool_use); + changed |= append_vec(&mut existing.permission_request, permission_request); + changed |= append_vec(&mut existing.post_tool_use, post_tool_use); + changed |= append_vec(&mut existing.pre_compact, pre_compact); + changed |= append_vec(&mut existing.post_compact, post_compact); + changed |= append_vec(&mut existing.session_start, session_start); + changed |= append_vec(&mut existing.user_prompt_submit, user_prompt_submit); + changed |= append_vec(&mut existing.subagent_start, subagent_start); + changed |= append_vec(&mut existing.subagent_stop, subagent_stop); + changed |= append_vec(&mut existing.stop, stop); + changed +} + +fn append_vec(existing: &mut Vec, mut incoming: Vec) -> bool { + let changed = !incoming.is_empty(); + existing.append(&mut incoming); + changed +} diff --git a/codex-rs/config/src/requirements_layers/layer.rs b/codex-rs/config/src/requirements_layers/layer.rs new file mode 100644 index 0000000000..d92024e117 --- /dev/null +++ b/codex-rs/config/src/requirements_layers/layer.rs @@ -0,0 +1,191 @@ +use crate::ConfigRequirementsToml; +use crate::ManagedHooksRequirementsToml; +use crate::RequirementSource; +use crate::RequirementsExecPolicyToml; +use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_absolute_path::AbsolutePathBufGuard; +use toml::Value as TomlValue; + +use super::stack::RequirementsCompositionError; + +#[derive(Clone, Debug)] +pub struct RequirementsLayerEntry { + pub(super) source: RequirementSource, + toml: RequirementsLayerToml, + base_dir: Option, +} + +impl RequirementsLayerEntry { + pub fn from_toml(source: RequirementSource, contents: impl Into) -> Self { + Self { + source, + toml: RequirementsLayerToml::String(contents.into()), + base_dir: None, + } + } + + pub fn from_toml_value(source: RequirementSource, value: TomlValue) -> Self { + Self { + source, + toml: RequirementsLayerToml::Value(value), + base_dir: None, + } + } + + pub fn with_base_dir(mut self, base_dir: AbsolutePathBuf) -> Self { + self.base_dir = Some(base_dir); + self + } +} + +#[derive(Clone, Debug)] +enum RequirementsLayerToml { + String(String), + Value(TomlValue), +} + +#[derive(Clone, Debug)] +pub(super) struct ComposableRequirementsLayer { + pub(super) source: RequirementSource, + pub(super) regular_toml: TomlValue, + pub(super) domain_fields: DomainMergedRequirementsFields, +} + +impl ComposableRequirementsLayer { + pub(super) fn from_entry( + layer: RequirementsLayerEntry, + hostname: Option<&str>, + ) -> Result { + let RequirementsLayerEntry { + source, + toml, + base_dir, + } = layer; + let (mut regular_toml, mut requirements) = { + let _guard = base_dir + .as_ref() + .map(|base_dir| AbsolutePathBufGuard::new(base_dir.as_path())); + let regular_toml = parse_layer_toml(&toml, &source)?; + let requirements = parse_layer_requirements(&toml, &source)?; + (regular_toml, requirements) + }; + + requirements.apply_remote_sandbox_config(hostname); + materialize_remote_sandbox_config(&mut regular_toml, &requirements)?; + strip_special_fields(&mut regular_toml); + + Ok(Self { + source, + regular_toml, + domain_fields: DomainMergedRequirementsFields { + rules: requirements.rules, + hooks: requirements.hooks, + permissions: requirements.permissions, + }, + }) + } +} + +#[derive(Clone, Debug)] +pub(super) struct DomainMergedRequirementsFields { + pub(super) rules: Option, + pub(super) hooks: Option, + pub(super) permissions: Option, +} + +fn parse_layer_toml( + toml: &RequirementsLayerToml, + source: &RequirementSource, +) -> Result { + match toml { + RequirementsLayerToml::String(contents) => { + toml::from_str(contents).map_err(|err: toml::de::Error| { + RequirementsCompositionError::Parse { + layer_source: source.clone(), + message: err.to_string(), + } + }) + } + RequirementsLayerToml::Value(value) => Ok(value.clone()), + } +} + +fn parse_layer_requirements( + toml: &RequirementsLayerToml, + source: &RequirementSource, +) -> Result { + match toml { + RequirementsLayerToml::String(contents) => { + toml::from_str(contents).map_err(|err: toml::de::Error| { + RequirementsCompositionError::Parse { + layer_source: source.clone(), + message: err.to_string(), + } + }) + } + RequirementsLayerToml::Value(value) => { + value.clone().try_into().map_err(|err: toml::de::Error| { + RequirementsCompositionError::Parse { + layer_source: source.clone(), + message: err.to_string(), + } + }) + } + } +} + +fn materialize_remote_sandbox_config( + layer_toml: &mut TomlValue, + requirements: &ConfigRequirementsToml, +) -> Result<(), RequirementsCompositionError> { + remove_top_level_field(layer_toml, "remote_sandbox_config"); + let Some(allowed_sandbox_modes) = requirements.allowed_sandbox_modes.as_ref() else { + return Ok(()); + }; + let Some(table) = layer_toml.as_table_mut() else { + return Ok(()); + }; + table.insert( + "allowed_sandbox_modes".to_string(), + toml_value_from_serializable(allowed_sandbox_modes)?, + ); + Ok(()) +} + +fn toml_value_from_serializable( + value: T, +) -> Result { + TomlValue::try_from(value).map_err(|err| RequirementsCompositionError::ComposedParse { + message: err.to_string(), + }) +} + +fn strip_special_fields(layer_toml: &mut TomlValue) { + remove_top_level_field(layer_toml, "rules"); + remove_top_level_field(layer_toml, "hooks"); + remove_nested_field_and_prune_empty(layer_toml, &["permissions", "filesystem", "deny_read"]); +} + +fn remove_top_level_field(value: &mut TomlValue, key: &str) -> Option { + value.as_table_mut()?.remove(key) +} + +fn remove_nested_field_and_prune_empty(value: &mut TomlValue, path: &[&str]) -> Option { + let (key, remaining) = path.split_first()?; + let table = value.as_table_mut()?; + if remaining.is_empty() { + return table.remove(*key); + } + + let removed = table + .get_mut(*key) + .and_then(|child| remove_nested_field_and_prune_empty(child, remaining)); + if table + .get(*key) + .and_then(TomlValue::as_table) + .is_some_and(toml::map::Map::is_empty) + { + table.remove(*key); + } + removed +} diff --git a/codex-rs/config/src/requirements_layers/mod.rs b/codex-rs/config/src/requirements_layers/mod.rs new file mode 100644 index 0000000000..52a23a29db --- /dev/null +++ b/codex-rs/config/src/requirements_layers/mod.rs @@ -0,0 +1,8 @@ +mod hooks; +mod layer; +mod permissions; +mod rules; +mod stack; + +pub use layer::RequirementsLayerEntry; +pub use stack::compose_requirements; diff --git a/codex-rs/config/src/requirements_layers/permissions.rs b/codex-rs/config/src/requirements_layers/permissions.rs new file mode 100644 index 0000000000..e4dd2f0ef2 --- /dev/null +++ b/codex-rs/config/src/requirements_layers/permissions.rs @@ -0,0 +1,82 @@ +//! `permissions.filesystem.deny_read` is intentionally additive across +//! requirements layers. Other `[permissions]` content stays in the regular TOML +//! merge path so permission profile tables follow config-style precedence. + +use crate::FilesystemDenyReadPattern; +use crate::RequirementSource; +use crate::Sourced; +use crate::config_requirements::FilesystemRequirementsToml; +use crate::config_requirements::PermissionsRequirementsToml; + +use super::stack::merge_output_source; + +#[derive(Default)] +pub(super) struct DenyReadMergeState { + deny_read: Vec, + source: Option, +} + +impl DenyReadMergeState { + pub(super) fn merge( + &mut self, + incoming: Option, + source: &RequirementSource, + ) { + let Some(incoming_deny_read) = incoming + .and_then(|permissions| permissions.filesystem) + .and_then(|filesystem| filesystem.deny_read) + .filter(|deny_read| !deny_read.is_empty()) + else { + return; + }; + + for pattern in incoming_deny_read { + if !self.deny_read.contains(&pattern) { + self.deny_read.push(pattern); + self.merge_source(source); + } + } + } + + pub(super) fn apply_to(self, target: &mut Option>) { + if self.deny_read.is_empty() { + return; + } + + let source = self.source.unwrap_or(RequirementSource::Unknown); + let Some(existing) = target.as_mut() else { + *target = Some(Sourced::new( + PermissionsRequirementsToml { + filesystem: Some(FilesystemRequirementsToml { + deny_read: Some(self.deny_read), + }), + profiles: Default::default(), + }, + source, + )); + return; + }; + + let filesystem = existing + .value + .filesystem + .get_or_insert_with(Default::default); + let deny_read = filesystem.deny_read.get_or_insert_with(Vec::new); + for pattern in self.deny_read { + if !deny_read.contains(&pattern) { + deny_read.push(pattern); + } + } + if existing.source != source { + existing.source = RequirementSource::composite([existing.source.clone(), source]); + } + } + + fn merge_source(&mut self, source: &RequirementSource) { + let Some(existing) = self.source.as_mut() else { + self.source = Some(source.clone()); + return; + }; + merge_output_source(existing, source); + } +} diff --git a/codex-rs/config/src/requirements_layers/rules.rs b/codex-rs/config/src/requirements_layers/rules.rs new file mode 100644 index 0000000000..1d73493a91 --- /dev/null +++ b/codex-rs/config/src/requirements_layers/rules.rs @@ -0,0 +1,26 @@ +//! Requirements rules are additive across layers. Higher-priority rules are +//! appended first so the final rule order keeps priority visible. + +use crate::RequirementSource; +use crate::RequirementsExecPolicyToml; +use crate::Sourced; + +use super::stack::merge_output_source; + +pub(super) fn merge( + target: &mut Option>, + incoming: Option, + source: &RequirementSource, +) { + let Some(incoming) = incoming else { + return; + }; + let Some(existing) = target.as_mut() else { + *target = Some(Sourced::new(incoming, source.clone())); + return; + }; + + let RequirementsExecPolicyToml { prefix_rules } = incoming; + existing.value.prefix_rules.extend(prefix_rules); + merge_output_source(&mut existing.source, source); +} diff --git a/codex-rs/config/src/requirements_layers/stack.rs b/codex-rs/config/src/requirements_layers/stack.rs new file mode 100644 index 0000000000..e93dd1c1da --- /dev/null +++ b/codex-rs/config/src/requirements_layers/stack.rs @@ -0,0 +1,286 @@ +//! Requirements layers are composed in the same order as config layers: lowest +//! precedence first, highest precedence last. Most fields use the same +//! TOML-level merge policy as config: lower-priority layers provide defaults, +//! and higher-priority layers override scalar/list values while recursively +//! extending tables. +//! +//! A few fields carry domain-specific meaning that raw TOML replacement would +//! break: +//! - `remote_sandbox_config` is evaluated within each layer before merging. +//! - `rules.prefix_rules` append high-priority rules first. +//! - `hooks` append high-priority event groups first while failing closed on +//! active managed-dir conflicts. +//! - `permissions.filesystem.deny_read` is a high-priority-first union across +//! layers. + +use crate::ConfigRequirementsToml; +use crate::ConfigRequirementsWithSources; +use crate::RequirementSource; +use crate::Sourced; +use crate::merge::merge_toml_values; +use std::io; +use thiserror::Error; +use toml::Value as TomlValue; + +use super::hooks::HookDirectoryField; +use super::hooks::HookMergeState; +use super::layer::ComposableRequirementsLayer; +use super::layer::RequirementsLayerEntry; +use super::permissions::DenyReadMergeState; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RequirementsCompositionError { + #[error("failed to parse requirements layer {layer_source}: {message}")] + Parse { + layer_source: RequirementSource, + message: String, + }, + #[error("failed to parse merged requirements: {message}")] + ComposedParse { message: String }, + #[error( + "failed to compose requirements field `{field}` between {existing_source} and {incoming_source}: {message}" + )] + Conflict { + field: String, + existing_source: RequirementSource, + incoming_source: RequirementSource, + message: String, + }, +} + +impl From for io::Error { + fn from(error: RequirementsCompositionError) -> Self { + io::Error::new(io::ErrorKind::InvalidData, error) + } +} + +pub fn compose_requirements( + layers: impl IntoIterator, +) -> Result, RequirementsCompositionError> { + let hostname = crate::host_name(); + compose_requirements_for_hostname(layers, hostname.as_deref()) +} + +pub(super) fn compose_requirements_for_hostname( + layers: impl IntoIterator, + hostname: Option<&str>, +) -> Result, RequirementsCompositionError> { + compose_requirements_for_hostname_and_hook_directory( + layers, + hostname, + HookDirectoryField::current_platform(), + ) +} + +pub(super) fn compose_requirements_for_hostname_and_hook_directory( + layers: impl IntoIterator, + hostname: Option<&str>, + hook_directory_field: HookDirectoryField, +) -> Result, RequirementsCompositionError> { + let mut stack = RequirementsLayerStack::new(hook_directory_field); + for layer in layers { + stack.add_layer(layer, hostname)?; + } + stack.compose() +} + +struct RequirementsLayerStack { + layers: Vec, + hook_directory_field: HookDirectoryField, +} + +impl RequirementsLayerStack { + fn new(hook_directory_field: HookDirectoryField) -> Self { + Self { + layers: Vec::new(), + hook_directory_field, + } + } + + fn add_layer( + &mut self, + layer: RequirementsLayerEntry, + hostname: Option<&str>, + ) -> Result<(), RequirementsCompositionError> { + self.layers + .push(ComposableRequirementsLayer::from_entry(layer, hostname)?); + Ok(()) + } + + fn compose( + self, + ) -> Result, RequirementsCompositionError> { + let Self { + layers, + hook_directory_field, + } = self; + + let mut merged_toml = TomlValue::Table(toml::map::Map::new()); + for layer in &layers { + merge_toml_values(&mut merged_toml, &layer.regular_toml); + } + + let requirements: ConfigRequirementsToml = + merged_toml.try_into().map_err(|err: toml::de::Error| { + RequirementsCompositionError::ComposedParse { + message: err.to_string(), + } + })?; + let mut output = ConfigRequirementsWithSources::default(); + populate_merged_regular_fields_with_sources(&mut output, requirements, &layers); + let mut rules = None; + let mut hooks = HookMergeState::new(hook_directory_field); + let mut hooks_output = None; + let mut deny_read = DenyReadMergeState::default(); + // Regular TOML fields are folded low-to-high like config. These custom + // fields append or union values, so process them high-to-low to keep + // priority order visible in the output. + for layer in layers.iter().rev() { + let domain_fields = &layer.domain_fields; + super::rules::merge(&mut rules, domain_fields.rules.clone(), &layer.source); + hooks.merge( + &mut hooks_output, + domain_fields.hooks.clone(), + &layer.source, + )?; + deny_read.merge(domain_fields.permissions.clone(), &layer.source); + } + output.rules = rules; + output.hooks = hooks_output; + deny_read.apply_to(&mut output.permissions); + + let output_is_empty = output.clone().into_toml().is_empty(); + Ok((!output_is_empty).then_some(output)) + } +} + +fn populate_merged_regular_fields_with_sources( + output: &mut ConfigRequirementsWithSources, + requirements: ConfigRequirementsToml, + layers: &[ComposableRequirementsLayer], +) { + macro_rules! set_sourced { + ($field:ident, $keys:expr) => { + if let Some(value) = $field { + output.$field = Some(Sourced::new( + value, + source_for_top_level_keys(layers, $keys), + )); + } + }; + } + + // Destructure without `..` so every new requirements field must choose + // whether it belongs in the regular TOML merge path or in a special merger. + let ConfigRequirementsToml { + allowed_approval_policies, + allowed_approvals_reviewers, + allowed_sandbox_modes, + allowed_permissions, + remote_sandbox_config: _, + allowed_web_search_modes, + allow_managed_hooks_only, + allow_appshots, + computer_use, + windows, + feature_requirements, + hooks: _, + mcp_servers, + plugins, + apps, + rules: _, + enforce_residency, + network, + permissions, + guardian_policy_config, + } = requirements; + + set_sourced!(allowed_approval_policies, &["allowed_approval_policies"]); + set_sourced!( + allowed_approvals_reviewers, + &["allowed_approvals_reviewers"] + ); + set_sourced!(allowed_sandbox_modes, &["allowed_sandbox_modes"]); + set_sourced!(allowed_permissions, &["allowed_permissions"]); + set_sourced!(allowed_web_search_modes, &["allowed_web_search_modes"]); + set_sourced!(allow_managed_hooks_only, &["allow_managed_hooks_only"]); + set_sourced!(allow_appshots, &["allow_appshots"]); + set_sourced!(computer_use, &["computer_use"]); + set_sourced!(windows, &["windows"]); + set_sourced!(feature_requirements, &["features", "feature_requirements"]); + set_sourced!(mcp_servers, &["mcp_servers"]); + set_sourced!(plugins, &["plugins"]); + set_sourced!(apps, &["apps"]); + set_sourced!(enforce_residency, &["enforce_residency"]); + set_sourced!(network, &["experimental_network"]); + set_sourced!(permissions, &["permissions"]); + + if let Some(guardian_policy_config) = + guardian_policy_config.filter(|value| !value.trim().is_empty()) + { + output.guardian_policy_config = Some(Sourced::new( + guardian_policy_config, + source_for_top_level_keys(layers, &["guardian_policy_config"]), + )); + } +} + +fn source_for_top_level_keys( + layers: &[ComposableRequirementsLayer], + keys: &[&str], +) -> RequirementSource { + let matching_layers = layers + .iter() + .filter_map(|layer| { + top_level_value_for_keys(&layer.regular_toml, keys).map(|value| (&layer.source, value)) + }) + .collect::>(); + let Some((winning_source, winning_value)) = matching_layers.last() else { + return RequirementSource::Unknown; + }; + let winning_source = (*winning_source).clone(); + + if !winning_value.is_table() { + return winning_source; + } + + let table_sources = matching_layers + .into_iter() + .rev() + .filter_map(|(source, value)| value.is_table().then_some(source.clone())) + .collect::>(); + if table_sources.len() > 1 { + RequirementSource::composite(table_sources) + } else { + winning_source + } +} + +fn top_level_value_for_keys<'a>(value: &'a TomlValue, keys: &[&str]) -> Option<&'a TomlValue> { + let table = value.as_table()?; + keys.iter().find_map(|key| table.get(*key)) +} + +pub(super) fn merge_output_source(existing: &mut RequirementSource, incoming: &RequirementSource) { + if existing != incoming { + *existing = RequirementSource::composite([existing.clone(), incoming.clone()]); + } +} + +pub(super) fn composition_conflict( + field: String, + existing_source: RequirementSource, + incoming_source: RequirementSource, + message: impl Into, +) -> RequirementsCompositionError { + RequirementsCompositionError::Conflict { + field, + existing_source, + incoming_source, + message: message.into(), + } +} + +#[cfg(test)] +#[path = "stack_tests.rs"] +mod tests; diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs new file mode 100644 index 0000000000..8acfd5f0d5 --- /dev/null +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -0,0 +1,922 @@ +use super::super::RequirementsLayerEntry; +use super::super::hooks::HookDirectoryField; +use super::RequirementsCompositionError; +use super::compose_requirements_for_hostname; +use super::compose_requirements_for_hostname_and_hook_directory; +use crate::ConfigRequirementsToml; +use crate::ConfigRequirementsWithSources; +use crate::RequirementSource; +use crate::Sourced; +use codex_protocol::protocol::AskForApproval; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; + +fn layer(id: &str, name: &str, contents: &str) -> RequirementsLayerEntry { + RequirementsLayerEntry::from_toml( + RequirementSource::EnterpriseManaged { + id: id.to_string(), + name: name.to_string(), + }, + contents, + ) +} + +fn compose( + layers: Vec, +) -> Result, RequirementsCompositionError> { + Ok( + compose_requirements_for_hostname(layers, /*hostname*/ None)? + .map(ConfigRequirementsWithSources::into_toml), + ) +} + +fn compose_with_hook_directory_field( + layers: Vec, + hook_directory_field: HookDirectoryField, +) -> Result, RequirementsCompositionError> { + Ok(compose_requirements_for_hostname_and_hook_directory( + layers, + /*hostname*/ None, + hook_directory_field, + )? + .map(ConfigRequirementsWithSources::into_toml)) +} + +fn expected_requirements(contents: impl AsRef) -> ConfigRequirementsToml { + toml::from_str(contents.as_ref()).expect("parse expected requirements TOML") +} + +#[test] +fn empty_layers_compose_to_none() { + let composed = compose(Vec::new()).expect("compose empty layers"); + assert_eq!(composed, None); +} + +#[test] +fn top_level_values_use_toml_priority() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +allowed_approval_policies = ["on-request"] +allowed_sandbox_modes = ["workspace-write"] +"#, + ), + layer( + "req_high", + "High", + r#" +allowed_approval_policies = ["never"] +allowed_sandbox_modes = ["read-only"] +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +allowed_approval_policies = ["never"] +allowed_sandbox_modes = ["read-only"] +"# + ) + ); +} + +#[test] +fn composition_strategy_applies_to_non_cloud_layers() { + let mdm_source = RequirementSource::MdmManagedPreferences { + domain: "com.openai.codex".to_string(), + key: "requirements_toml_base64".to_string(), + }; + let system_file = if cfg!(windows) { + "C:\\requirements.toml" + } else { + "/etc/codex/requirements.toml" + }; + let system_source = RequirementSource::SystemRequirementsToml { + file: AbsolutePathBuf::from_absolute_path(system_file).expect("absolute path"), + }; + let high_path = if cfg!(windows) { + "C:\\secret" + } else { + "/secret" + }; + let low_path = if cfg!(windows) { + "C:\\other-secret" + } else { + "/other-secret" + }; + + let composed = compose_requirements_for_hostname( + vec![ + RequirementsLayerEntry::from_toml( + system_source, + format!( + r#" +allowed_approval_policies = ["on-request"] + +[features] +shared = false +system = true + +[[rules.prefix_rules]] +pattern = [{{ token = "npm" }}] +decision = "prompt" + +[permissions.filesystem] +deny_read = [{low_path:?}] +"# + ), + ), + RequirementsLayerEntry::from_toml( + mdm_source.clone(), + format!( + r#" +allowed_approval_policies = ["never"] + +[features] +shared = true + +[[rules.prefix_rules]] +pattern = [{{ token = "git" }}] +decision = "forbidden" + +[permissions.filesystem] +deny_read = [{high_path:?}] +"# + ), + ), + ], + /*hostname*/ None, + ) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed.clone().into_toml(), + expected_requirements(format!( + r#" +allowed_approval_policies = ["never"] + +[features] +shared = true +system = true + +[[rules.prefix_rules]] +pattern = [{{ token = "git" }}] +decision = "forbidden" + +[[rules.prefix_rules]] +pattern = [{{ token = "npm" }}] +decision = "prompt" + +[permissions.filesystem] +deny_read = [{high_path:?}, {low_path:?}] +"# + )) + ); + assert_eq!( + composed.allowed_approval_policies, + Some(Sourced::new(vec![AskForApproval::Never], mdm_source)) + ); +} + +#[test] +fn single_regular_layer_keeps_enterprise_managed_source() { + let composed = compose_requirements_for_hostname( + vec![layer( + "req_1", + "Security baseline", + r#" +allow_managed_hooks_only = true +"#, + )], + /*hostname*/ None, + ) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed.allow_managed_hooks_only, + Some(Sourced::new( + /*value*/ true, + RequirementSource::EnterpriseManaged { + id: "req_1".to_string(), + name: "Security baseline".to_string(), + }, + )) + ); +} + +#[test] +fn regular_toml_merge_recurses_into_tables() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[features] +beta = false +shared = false + +[apps.connector_1] +enabled = false + +[apps.connector_1.tools.search] +approval_mode = "prompt" + +[apps.connector_1.tools.list] +approval_mode = "prompt" +"#, + ), + layer( + "req_high", + "High", + r#" +[features] +alpha = true +shared = true + +[apps.connector_1] +enabled = true + +[apps.connector_1.tools.search] +approval_mode = "approve" +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[features] +alpha = true +beta = false +shared = true + +[apps.connector_1] +enabled = true + +[apps.connector_1.tools.list] +approval_mode = "prompt" + +[apps.connector_1.tools.search] +approval_mode = "approve" +"# + ) + ); +} + +#[test] +fn merged_table_source_is_composite_in_priority_order() { + let high_source = RequirementSource::EnterpriseManaged { + id: "req_high".to_string(), + name: "High".to_string(), + }; + let low_source = RequirementSource::EnterpriseManaged { + id: "req_low".to_string(), + name: "Low".to_string(), + }; + let composed = compose_requirements_for_hostname( + vec![ + RequirementsLayerEntry::from_toml( + low_source.clone(), + r#" +[features] +beta = true +"#, + ), + RequirementsLayerEntry::from_toml( + high_source.clone(), + r#" +[features] +alpha = true +"#, + ), + ], + /*hostname*/ None, + ) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed.feature_requirements.expect("features"), + Sourced::new( + crate::FeatureRequirementsToml { + entries: BTreeMap::from([("alpha".to_string(), true), ("beta".to_string(), true),]), + }, + RequirementSource::composite([high_source, low_source]), + ) + ); +} + +#[test] +fn mcp_requirements_use_regular_toml_merge() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[mcp_servers.shared.identity] +command = "low-mcp" + +[mcp_servers.low.identity] +url = "https://low.example.com/mcp" +"#, + ), + layer( + "req_high", + "High", + r#" +[mcp_servers.shared.identity] +command = "high-mcp" +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[mcp_servers.low.identity] +url = "https://low.example.com/mcp" + +[mcp_servers.shared.identity] +command = "high-mcp" +"# + ) + ); +} + +#[test] +fn network_maps_use_regular_toml_merge() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[experimental_network.domains] +"example.com" = "deny" +"low.example.com" = "deny" +"internal.example.com" = "allow" + +[experimental_network.unix_sockets] +"/tmp/shared.sock" = "deny" +"/tmp/low.sock" = "allow" +"/tmp/admin.sock" = "allow" +"#, + ), + layer( + "req_high", + "High", + r#" +[experimental_network.domains] +"example.com" = "allow" +"high.example.com" = "allow" +"internal.example.com" = "deny" + +[experimental_network.unix_sockets] +"/tmp/shared.sock" = "allow" +"/tmp/high.sock" = "allow" +"/tmp/admin.sock" = "deny" +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[experimental_network.domains] +"example.com" = "allow" +"high.example.com" = "allow" +"internal.example.com" = "deny" +"low.example.com" = "deny" + +[experimental_network.unix_sockets] +"/tmp/admin.sock" = "deny" +"/tmp/high.sock" = "allow" +"/tmp/low.sock" = "allow" +"/tmp/shared.sock" = "allow" +"# + ) + ); +} + +#[test] +fn windows_requirements_use_regular_toml_merge() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[windows] +allowed_sandbox_implementations = ["unelevated"] +"#, + ), + layer( + "req_high", + "High", + r#" +[windows] +allowed_sandbox_implementations = ["elevated"] +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[windows] +allowed_sandbox_implementations = ["elevated"] +"# + ) + ); +} + +#[test] +fn remote_sandbox_config_is_applied_per_layer() { + let composed = compose_requirements_for_hostname( + vec![ + layer( + "req_low", + "Low", + r#" +allowed_sandbox_modes = ["read-only"] +"#, + ), + layer( + "req_high", + "High", + r#" +[[remote_sandbox_config]] +hostname_patterns = ["build-*.example.com"] +allowed_sandbox_modes = ["workspace-write"] +"#, + ), + ], + Some("BUILD-01.EXAMPLE.COM."), + ) + .expect("compose requirements") + .expect("requirements present") + .into_toml(); + + assert_eq!( + composed, + expected_requirements( + r#" +allowed_sandbox_modes = ["workspace-write"] +"# + ) + ); +} + +#[test] +fn unmatched_remote_sandbox_config_does_not_shadow_lower_layers() { + let composed = compose_requirements_for_hostname( + vec![ + layer( + "req_low", + "Low", + r#" +allowed_sandbox_modes = ["read-only"] +"#, + ), + layer( + "req_high", + "High", + r#" +[[remote_sandbox_config]] +hostname_patterns = ["mac-*.example.com"] +allowed_sandbox_modes = ["workspace-write"] +"#, + ), + ], + Some("linux-01.example.com"), + ) + .expect("compose requirements") + .expect("requirements present") + .into_toml(); + + assert_eq!( + composed, + expected_requirements( + r#" +allowed_sandbox_modes = ["read-only"] +"# + ) + ); +} + +#[test] +fn rules_are_appended_in_priority_order() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[[rules.prefix_rules]] +pattern = [{ token = "npm" }] +decision = "prompt" +"#, + ), + layer( + "req_high", + "High", + r#" +[[rules.prefix_rules]] +pattern = [{ token = "git" }] +decision = "forbidden" +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[[rules.prefix_rules]] +pattern = [{ token = "git" }] +decision = "forbidden" + +[[rules.prefix_rules]] +pattern = [{ token = "npm" }] +decision = "prompt" +"# + ) + ); +} + +#[test] +fn hooks_append_groups_and_reject_conflicting_managed_dirs() { + let composed = compose_with_hook_directory_field( + vec![ + layer( + "req_low", + "Low", + r#" +[hooks] +managed_dir = "/managed/hooks" + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"#, + ), + layer( + "req_high", + "High", + r#" +[hooks] +managed_dir = "/managed/hooks" + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" +"#, + ), + ], + HookDirectoryField::ManagedDir, + ) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[hooks] +managed_dir = "/managed/hooks" + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"# + ) + ); + + let err = compose_with_hook_directory_field( + vec![ + layer( + "req_low", + "Low", + r#" +[hooks] +managed_dir = "/managed/low" +"#, + ), + layer( + "req_high", + "High", + r#" +[hooks] +managed_dir = "/managed/high" +"#, + ), + ], + HookDirectoryField::ManagedDir, + ) + .expect_err("conflicting managed dirs should fail closed"); + assert!(err.to_string().contains("hooks.managed_dir")); + assert!(err.to_string().contains("High (req_high)")); + assert!(err.to_string().contains("Low (req_low)")); +} + +#[test] +fn active_windows_managed_dir_conflicts_fail_closed() { + let err = compose_with_hook_directory_field( + vec![ + layer( + "req_low", + "Low", + r#" +[hooks] +windows_managed_dir = 'C:\managed\low' +"#, + ), + layer( + "req_high", + "High", + r#" +[hooks] +windows_managed_dir = 'C:\managed\high' +"#, + ), + ], + HookDirectoryField::WindowsManagedDir, + ) + .expect_err("conflicting windows managed dirs should fail closed"); + + assert!(err.to_string().contains("hooks.windows_managed_dir")); + assert!(err.to_string().contains("High (req_high)")); + assert!(err.to_string().contains("Low (req_low)")); +} + +#[test] +fn inactive_hook_dir_conflicts_do_not_fail_composition() { + let composed = compose_with_hook_directory_field( + vec![ + layer( + "req_low", + "Low", + r#" +[hooks] +managed_dir = "/managed/hooks" +windows_managed_dir = 'C:\managed\low' + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"#, + ), + layer( + "req_high", + "High", + r#" +[hooks] +managed_dir = "/managed/hooks" +windows_managed_dir = 'C:\managed\high' + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" +"#, + ), + ], + HookDirectoryField::ManagedDir, + ) + .expect("inactive windows managed dir conflict should not fail") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[hooks] +managed_dir = "/managed/hooks" +windows_managed_dir = 'C:\managed\high' + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"# + ) + ); + + let composed = compose_with_hook_directory_field( + vec![ + layer( + "req_low", + "Low", + r#" +[hooks] +managed_dir = "/managed/low" +windows_managed_dir = 'C:\managed\hooks' + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"#, + ), + layer( + "req_high", + "High", + r#" +[hooks] +managed_dir = "/managed/high" +windows_managed_dir = 'C:\managed\hooks' + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" +"#, + ), + ], + HookDirectoryField::WindowsManagedDir, + ) + .expect("inactive managed dir conflict should not fail") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[hooks] +managed_dir = "/managed/high" +windows_managed_dir = 'C:\managed\hooks' + +[[hooks.PreToolUse]] +matcher = "Edit" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "high" + +[[hooks.PreToolUse]] +matcher = "Bash" + +[[hooks.PreToolUse.hooks]] +type = "command" +command = "low" +"# + ) + ); +} + +#[test] +fn permissions_deny_read_unions_while_profiles_use_regular_toml_merge() { + let high_path = if cfg!(windows) { + "C:\\secret" + } else { + "/secret" + }; + let low_path = if cfg!(windows) { + "C:\\other-secret" + } else { + "/other-secret" + }; + let composed = compose(vec![ + layer( + "req_low", + "Low", + &format!( + r#" +[permissions.filesystem] +deny_read = [{high_path:?}, {low_path:?}] + +[permissions.managed-standard] +description = "Low profile" +extends = ":workspace" +"# + ), + ), + layer( + "req_high", + "High", + &format!( + r#" +[permissions.filesystem] +deny_read = [{high_path:?}] + +[permissions.managed-standard] +description = "High profile" +"# + ), + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements(format!( + r#" +[permissions.filesystem] +deny_read = [{high_path:?}, {low_path:?}] + +[permissions.managed-standard] +description = "High profile" +extends = ":workspace" +"# + )) + ); +} + +#[test] +fn deny_read_only_layers_do_not_leave_empty_permissions_tables() { + let path = if cfg!(windows) { + "C:\\secret" + } else { + "/secret" + }; + let composed = compose(vec![layer( + "req_high", + "High", + &format!( + r#" +[permissions.filesystem] +deny_read = [{path:?}] +"# + ), + )]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements(format!( + r#" +[permissions.filesystem] +deny_read = [{path:?}] +"# + )) + ); +} + +#[test] +fn parse_error_names_layer() { + let err = compose(vec![layer( + "req_bad", + "Bad layer", + "allowed_approval_policies = [1]", + )]) + .expect_err("invalid layer should fail"); + + assert!(err.to_string().contains("Bad layer (req_bad)")); + assert!(err.to_string().contains("allowed_approval_policies")); +} diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index df0de3b1cd..e0d2c70671 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -99,7 +99,7 @@ pub fn canonicalize(value: &Value) -> Value { Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()), Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize(child)); diff --git a/codex-rs/config/src/state.rs b/codex-rs/config/src/state.rs index 3ec0bfb314..e4d03c3d3c 100644 --- a/codex-rs/config/src/state.rs +++ b/codex-rs/config/src/state.rs @@ -97,33 +97,47 @@ impl LoaderOverrides { pub struct ConfigLayerEntry { pub name: ConfigLayerSource, pub config: TomlValue, - pub raw_toml: Option, pub version: String, pub disabled_reason: Option, + raw_toml: Option, hooks_config_folder_override: Option, } +#[derive(Debug, Clone, PartialEq)] +struct RawTomlLayer { + contents: String, + base_dir: AbsolutePathBuf, +} + impl ConfigLayerEntry { pub fn new(name: ConfigLayerSource, config: TomlValue) -> Self { let version = version_for_toml(&config); Self { name, config, - raw_toml: None, version, disabled_reason: None, + raw_toml: None, hooks_config_folder_override: None, } } - pub fn new_with_raw_toml(name: ConfigLayerSource, config: TomlValue, raw_toml: String) -> Self { + pub fn new_with_raw_toml( + name: ConfigLayerSource, + config: TomlValue, + raw_toml: String, + raw_toml_base_dir: AbsolutePathBuf, + ) -> Self { let version = version_for_toml(&config); Self { name, config, - raw_toml: Some(raw_toml), version, disabled_reason: None, + raw_toml: Some(RawTomlLayer { + contents: raw_toml, + base_dir: raw_toml_base_dir, + }), hooks_config_folder_override: None, } } @@ -137,9 +151,9 @@ impl ConfigLayerEntry { Self { name, config, - raw_toml: None, version, disabled_reason: Some(disabled_reason.into()), + raw_toml: None, hooks_config_folder_override: None, } } @@ -149,7 +163,13 @@ impl ConfigLayerEntry { } pub fn raw_toml(&self) -> Option<&str> { - self.raw_toml.as_deref() + self.raw_toml + .as_ref() + .map(|raw_toml| raw_toml.contents.as_str()) + } + + pub fn raw_toml_base_dir(&self) -> Option<&AbsolutePathBuf> { + self.raw_toml.as_ref().map(|raw_toml| &raw_toml.base_dir) } pub(crate) fn with_hooks_config_folder_override( @@ -181,6 +201,7 @@ impl ConfigLayerEntry { match &self.name { ConfigLayerSource::Mdm { .. } => None, ConfigLayerSource::System { file } => file.parent(), + ConfigLayerSource::EnterpriseManaged { .. } => None, ConfigLayerSource::User { file, .. } => file.parent(), ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder.clone()), ConfigLayerSource::SessionFlags => None, diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 5b1e7d4cc8..93392f1f7e 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -23,6 +23,9 @@ use serde::Serialize; use serde::de::Error as SerdeError; use std::collections::BTreeMap; +/// Highest function key supported by portable TUI keymap configuration. +pub const MAX_FUNCTION_KEY: u8 = 24; + /// Normalized string representation of a single key event (for example `ctrl-a`). /// /// The parser accepts a small alias set (for example `escape` -> `esc`, @@ -115,6 +118,8 @@ pub struct TuiGlobalKeymap { #[serde(deny_unknown_fields)] #[schemars(deny_unknown_fields)] pub struct TuiChatKeymap { + /// Interrupt the active turn. + pub interrupt_turn: Option, /// Decrease the active reasoning effort. pub decrease_reasoning_effort: Option, /// Increase the active reasoning effort. @@ -221,8 +226,12 @@ pub struct TuiVimNormalKeymap { pub move_line_end: Option, /// Delete character under cursor (`x`). pub delete_char: Option, + /// Delete character under cursor and enter insert mode (`s`). + pub substitute_char: Option, /// Delete from cursor to end of line (`D`). pub delete_to_line_end: Option, + /// Change from cursor to end of line and enter insert mode (`C`). + pub change_to_line_end: Option, /// Yank the entire line (`Y`). pub yank_line: Option, /// Paste after cursor (`p`). @@ -231,6 +240,8 @@ pub struct TuiVimNormalKeymap { pub start_delete_operator: Option, /// Begin yank operator; next key selects motion (`y`). pub start_yank_operator: Option, + /// Begin change operator; next keys select a text object. + pub start_change_operator: Option, /// Cancel a pending operator and return to normal mode. pub cancel_operator: Option, } @@ -266,10 +277,39 @@ pub struct TuiVimOperatorKeymap { pub motion_line_start: Option, /// Motion: to end of line (`$`). pub motion_line_end: Option, + /// Select an inner text object after an operator. + pub select_inner_text_object: Option, + /// Select an around text object after an operator. + pub select_around_text_object: Option, /// Cancel the pending operator and return to normal mode. pub cancel: Option, } +/// Vim text-object keybindings for modal editing inside text areas. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[serde(deny_unknown_fields)] +#[schemars(deny_unknown_fields)] +pub struct TuiVimTextObjectKeymap { + /// Text object: word. + pub word: Option, + /// Text object: whitespace-delimited WORD. + pub big_word: Option, + /// Text object: parentheses. + pub parentheses: Option, + /// Text object: brackets. + pub brackets: Option, + /// Text object: braces. + pub braces: Option, + /// Text object: double quotes. + pub double_quote: Option, + /// Text object: single quotes. + pub single_quote: Option, + /// Text object: backticks. + pub backtick: Option, + /// Cancel the pending text-object command. + pub cancel: Option, +} + /// Pager context keybindings for transcript and static overlays. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] #[serde(deny_unknown_fields)] @@ -374,6 +414,8 @@ pub struct TuiKeymap { #[serde(default)] pub vim_operator: TuiVimOperatorKeymap, #[serde(default)] + pub vim_text_object: TuiVimTextObjectKeymap, + #[serde(default)] pub pager: TuiPagerKeymap, #[serde(default)] pub list: TuiListKeymap, @@ -513,14 +555,14 @@ fn normalize_key_name(key: &str, original: &str) -> Result { if let Some(number) = alias.strip_prefix('f') && let Ok(number) = number.parse::() - && (1..=12).contains(&number) + && (1..=MAX_FUNCTION_KEY).contains(&number) { return Ok(alias.to_string()); } Err(format!( "unknown key `{key}` in keybinding `{original}`. \ -Use a printable character (for example `a`), function keys (`f1`-`f12`), \ +Use a printable character (for example `a`), function keys (`f1`-`f{MAX_FUNCTION_KEY}`), \ or one of: enter, tab, backspace, esc, delete, arrows, home/end, page-up/page-down, space, minus.\n\ See the Codex keymap documentation for supported actions and examples." )) @@ -560,6 +602,20 @@ mod tests { ); } + #[test] + fn misspelled_vim_text_object_action_is_rejected() { + let toml_input = r#" + [vim_text_object] + double_quotes = "shift-quote" + "#; + let err = toml::from_str::(toml_input) + .expect_err("expected unknown vim text object action"); + assert!( + err.to_string().contains("double_quotes"), + "expected error to mention misspelled field, got: {err}" + ); + } + #[test] fn removed_backtrack_actions_are_rejected() { for (context, action) in [ @@ -621,4 +677,11 @@ mod tests { assert_eq!(keymap, expected_keymap); } } + + #[test] + fn function_keys_through_f24_are_accepted() { + assert_eq!(normalize_keybinding_spec("F13"), Ok("f13".to_string())); + assert_eq!(normalize_keybinding_spec("f24"), Ok("f24".to_string())); + assert!(normalize_keybinding_spec("f25").is_err()); + } } diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 30d297030a..598b7fdee7 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -31,6 +31,7 @@ use serde::Serialize; pub use crate::tui_keymap::KeybindingSpec; pub use crate::tui_keymap::KeybindingsSpec; +pub use crate::tui_keymap::MAX_FUNCTION_KEY; pub use crate::tui_keymap::TuiApprovalKeymap; pub use crate::tui_keymap::TuiChatKeymap; pub use crate::tui_keymap::TuiComposerKeymap; diff --git a/codex-rs/connectors/src/metadata.rs b/codex-rs/connectors/src/metadata.rs index 64becb766d..0a7eebe4e4 100644 --- a/codex-rs/connectors/src/metadata.rs +++ b/codex-rs/connectors/src/metadata.rs @@ -5,7 +5,11 @@ pub fn connector_display_label(connector: &AppInfo) -> String { } pub fn connector_mention_slug(connector: &AppInfo) -> String { - crate::connector_name_slug(&connector_display_label(connector)) + connector_mention_slug_from_name(&connector_display_label(connector)) +} + +pub fn connector_mention_slug_from_name(name: &str) -> String { + crate::connector_name_slug(name) } pub fn connector_install_url(name: &str, connector_id: &str) -> String { diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index 83df070dd3..d0096e4455 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -31,6 +31,7 @@ codex-utils-plugins = { workspace = true } chrono = { workspace = true } dirs = { workspace = true } flate2 = { workspace = true } +indexmap = { workspace = true, features = ["serde"] } reqwest = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index 9e0ef7c471..d51771085d 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -5,6 +5,7 @@ use crate::manifest::load_plugin_manifest; use crate::marketplace::MarketplacePluginSource; use crate::marketplace::list_marketplaces; use crate::marketplace::load_marketplace; +use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; use crate::remote::RemoteInstalledPlugin; use crate::store::PluginStore; use crate::store::plugin_version_for_source; @@ -32,6 +33,7 @@ use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_plugins::find_plugin_manifest_path; +use indexmap::IndexMap; use serde::Deserialize; use serde_json::Map as JsonMap; use serde_json::Value as JsonValue; @@ -99,7 +101,7 @@ impl PluginMcpFile { #[serde(rename_all = "camelCase")] struct PluginAppFile { #[serde(default)] - apps: HashMap, + apps: IndexMap, } #[derive(Debug, Default, Deserialize)] @@ -112,10 +114,15 @@ pub async fn load_plugins_from_layer_stack( extra_plugins: HashMap, store: &PluginStore, restriction_product: Option, + prefer_remote_curated_conflicts: bool, ) -> PluginLoadOutcome { let skill_config_rules = skill_config_rules_from_stack(config_layer_stack); - let mut configured_plugins = configured_plugins_from_stack(config_layer_stack); - configured_plugins.extend(extra_plugins); + let configured_plugins = merge_configured_plugins_with_remote_installed( + configured_plugins_from_stack(config_layer_stack), + extra_plugins, + store, + prefer_remote_curated_conflicts, + ); let mut configured_plugins: Vec<_> = configured_plugins.into_iter().collect(); configured_plugins.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); @@ -148,6 +155,61 @@ pub async fn load_plugins_from_layer_stack( PluginLoadOutcome::from_plugins(plugins) } +fn merge_configured_plugins_with_remote_installed( + mut configured_plugins: HashMap, + extra_plugins: HashMap, + store: &PluginStore, + prefer_remote_curated_conflicts: bool, +) -> HashMap { + let local_curated_installed_plugin_keys = configured_plugins + .keys() + .filter_map(|plugin_key| { + installed_plugin_name_for_marketplace( + plugin_key, + OPENAI_CURATED_MARKETPLACE_NAME, + store, + ) + .map(|plugin_name| (plugin_name, plugin_key.clone())) + }) + .collect::>(); + + for (plugin_key, plugin_config) in extra_plugins { + let remote_curated_plugin_name = installed_plugin_name_for_marketplace( + &plugin_key, + REMOTE_GLOBAL_MARKETPLACE_NAME, + store, + ); + let local_curated_plugin_key = remote_curated_plugin_name + .as_ref() + .and_then(|plugin_name| local_curated_installed_plugin_keys.get(plugin_name)); + + if let Some(local_curated_plugin_key) = local_curated_plugin_key { + if prefer_remote_curated_conflicts { + configured_plugins.remove(local_curated_plugin_key); + } else { + continue; + } + } + + configured_plugins.insert(plugin_key, plugin_config); + } + + configured_plugins +} + +fn installed_plugin_name_for_marketplace( + plugin_key: &str, + marketplace_name: &str, + store: &PluginStore, +) -> Option { + let plugin_id = PluginId::parse(plugin_key).ok()?; + if plugin_id.marketplace_name != marketplace_name { + return None; + } + store.active_plugin_root(&plugin_id)?; + Some(plugin_id.plugin_name) +} + pub fn remote_installed_plugins_to_config( plugins: &[RemoteInstalledPlugin], store: &PluginStore, @@ -883,10 +945,7 @@ async fn load_apps_from_paths( } }; - let mut apps: Vec = parsed.apps.into_values().collect(); - apps.sort_unstable_by(|left, right| left.id.cmp(&right.id)); - - connector_ids.extend(apps.into_iter().filter_map(|app| { + connector_ids.extend(parsed.apps.into_values().filter_map(|app| { if app.id.trim().is_empty() { warn!( plugin = %plugin_root.display(), @@ -898,7 +957,8 @@ async fn load_apps_from_paths( } })); } - connector_ids.dedup(); + let mut seen_connector_ids = HashSet::new(); + connector_ids.retain(|connector_id| seen_connector_ids.insert(connector_id.0.clone())); connector_ids } diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index ec3e74dd16..ff596273f9 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -492,6 +492,7 @@ impl PluginsManager { self.remote_installed_plugin_configs(), &self.store, self.restriction_product, + config.remote_plugin_enabled, ) .await; log_plugin_load_errors(&outcome); @@ -537,6 +538,7 @@ impl PluginsManager { self.remote_installed_plugin_configs(), &self.store, self.restriction_product, + config.remote_plugin_enabled, ) .await } diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 0131ef22ed..905d120a8e 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -133,10 +133,14 @@ async fn load_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput { } fn remote_installed_linear_plugin() -> RemoteInstalledPlugin { + remote_installed_plugin("linear") +} + +fn remote_installed_plugin(name: &str) -> RemoteInstalledPlugin { RemoteInstalledPlugin { marketplace_name: "openai-curated-remote".to_string(), - id: "plugins~Plugin_linear".to_string(), - name: "linear".to_string(), + id: format!("plugins~Plugin_{name}"), + name: name.to_string(), enabled: true, install_policy: codex_app_server_protocol::PluginInstallPolicy::Available, auth_policy: codex_app_server_protocol::PluginAuthPolicy::OnUse, @@ -146,6 +150,18 @@ fn remote_installed_linear_plugin() -> RemoteInstalledPlugin { } } +fn write_cached_plugin(codex_home: &Path, marketplace_name: &str, plugin_name: &str) { + write_plugin_with_version( + &codex_home + .join("plugins/cache") + .join(marketplace_name) + .join(plugin_name), + "local", + plugin_name, + /*manifest_version*/ Some("local"), + ); +} + #[tokio::test] async fn load_plugins_loads_default_skills_and_mcp_servers() { let codex_home = TempDir::new().unwrap(); @@ -352,6 +368,92 @@ remote_plugin = true assert_eq!(outcome, PluginLoadOutcome::default()); } +#[tokio::test] +async fn remote_installed_cache_prefers_local_curated_conflicts_when_remote_plugin_disabled() { + let codex_home = TempDir::new().unwrap(); + write_file( + &codex_home.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +remote_plugin = false + +[plugins."linear@openai-curated"] +enabled = true + +[plugins."calendar@openai-curated"] +enabled = true +"#, + ); + write_cached_plugin(codex_home.path(), "openai-curated", "linear"); + write_cached_plugin(codex_home.path(), "openai-curated", "calendar"); + write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear"); + write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only"); + + let config = load_config(codex_home.path(), codex_home.path()).await; + let manager = PluginsManager::new(codex_home.path().to_path_buf()); + manager.write_remote_installed_plugins_cache(vec![ + remote_installed_plugin("linear"), + remote_installed_plugin("remote-only"), + ]); + + let outcome = manager.plugins_for_config(&config).await; + assert_eq!( + outcome + .plugins() + .iter() + .map(|plugin| plugin.config_name.clone()) + .collect::>(), + vec![ + "calendar@openai-curated".to_string(), + "linear@openai-curated".to_string(), + "remote-only@openai-curated-remote".to_string(), + ] + ); +} + +#[tokio::test] +async fn remote_installed_cache_prefers_remote_curated_conflicts_when_remote_plugin_enabled() { + let codex_home = TempDir::new().unwrap(); + write_file( + &codex_home.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +remote_plugin = true + +[plugins."linear@openai-curated"] +enabled = true + +[plugins."calendar@openai-curated"] +enabled = true +"#, + ); + write_cached_plugin(codex_home.path(), "openai-curated", "linear"); + write_cached_plugin(codex_home.path(), "openai-curated", "calendar"); + write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear"); + write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only"); + + let config = load_config(codex_home.path(), codex_home.path()).await; + let manager = PluginsManager::new(codex_home.path().to_path_buf()); + manager.write_remote_installed_plugins_cache(vec![ + remote_installed_plugin("linear"), + remote_installed_plugin("remote-only"), + ]); + + let outcome = manager.plugins_for_config(&config).await; + assert_eq!( + outcome + .plugins() + .iter() + .map(|plugin| plugin.config_name.clone()) + .collect::>(), + vec![ + "calendar@openai-curated".to_string(), + "linear@openai-curated-remote".to_string(), + "remote-only@openai-curated-remote".to_string(), + ] + ); +} + #[tokio::test] async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metadata() { let codex_home = TempDir::new().unwrap(); @@ -857,6 +959,43 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() { ); } +#[tokio::test] +async fn load_plugins_ignores_invalid_manifest_skills_shape() { + let codex_home = TempDir::new().unwrap(); + let plugin_root = codex_home + .path() + .join("plugins/cache") + .join("test/sample/local"); + + write_file( + &plugin_root.join(".codex-plugin/plugin.json"), + r#"{ + "name": "sample", + "skills": ["./custom-skills/"] +}"#, + ); + write_file( + &plugin_root.join("skills/default-skill/SKILL.md"), + "---\nname: default-skill\ndescription: default skill\n---\n", + ); + write_file( + &plugin_root.join("custom-skills/custom-skill/SKILL.md"), + "---\nname: custom-skill\ndescription: custom skill\n---\n", + ); + + let outcome = load_plugins_from_config( + &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), + codex_home.path(), + ) + .await; + + assert_eq!(outcome.plugins()[0].error, None); + assert_eq!( + outcome.plugins()[0].skill_roots, + vec![plugin_root.join("skills").abs()] + ); +} + #[tokio::test] async fn load_plugins_preserves_disabled_plugins_without_effective_contributions() { let codex_home = TempDir::new().unwrap(); @@ -985,6 +1124,50 @@ async fn effective_apps_dedupes_connector_ids_across_plugins() { ); } +#[tokio::test] +async fn effective_apps_preserves_app_config_order() { + let codex_home = TempDir::new().unwrap(); + let plugin_root = codex_home + .path() + .join("plugins/cache") + .join("test/sample/local"); + + write_file( + &plugin_root.join(".codex-plugin/plugin.json"), + r#"{"name":"sample"}"#, + ); + write_file( + &plugin_root.join(".app.json"), + r#"{ + "apps": { + "slack": { + "id": "connector_slack" + }, + "github": { + "id": "connector_github" + }, + "slack-copy": { + "id": "connector_slack" + } + } +}"#, + ); + + let outcome = load_plugins_from_config( + &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), + codex_home.path(), + ) + .await; + + assert_eq!( + outcome.effective_apps(), + vec![ + AppConnectorId("connector_slack".to_string()), + AppConnectorId("connector_github".to_string()), + ] + ); +} + #[test] fn capability_index_filters_inactive_and_zero_capability_plugins() { let codex_home = TempDir::new().unwrap(); @@ -3669,6 +3852,7 @@ async fn load_plugins_ignores_project_config_files() { std::collections::HashMap::new(), &PluginStore::new(codex_home.path().to_path_buf()), Some(Product::Codex), + /*prefer_remote_curated_conflicts*/ false, ) .await; diff --git a/codex-rs/core-plugins/src/manifest.rs b/codex-rs/core-plugins/src/manifest.rs index 6de7f820b8..606ca81475 100644 --- a/codex-rs/core-plugins/src/manifest.rs +++ b/codex-rs/core-plugins/src/manifest.rs @@ -23,7 +23,7 @@ struct RawPluginManifest { // Keep manifest paths as raw strings so we can validate the required `./...` syntax before // resolving them under the plugin root. #[serde(default)] - skills: Option, + skills: Option, #[serde(default)] mcp_servers: Option, #[serde(default)] @@ -127,6 +127,13 @@ enum RawPluginManifestDefaultPromptEntry { Invalid(JsonValue), } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum RawPluginManifestPath { + Path(String), + Invalid(JsonValue), +} + #[derive(Debug, Deserialize)] #[serde(untagged)] enum RawPluginManifestHooks { @@ -238,7 +245,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option { description, keywords, paths: PluginManifestPaths { - skills: resolve_manifest_path(plugin_root, "skills", skills.as_deref()), + skills: resolve_manifest_path_value(plugin_root, "skills", skills.as_ref()), mcp_servers: resolve_manifest_path( plugin_root, "mcpServers", @@ -394,6 +401,23 @@ fn json_value_type(value: &JsonValue) -> &'static str { } } +fn resolve_manifest_path_value( + plugin_root: &Path, + field: &'static str, + path: Option<&RawPluginManifestPath>, +) -> Option { + match path? { + RawPluginManifestPath::Path(path) => resolve_manifest_path(plugin_root, field, Some(path)), + RawPluginManifestPath::Invalid(value) => { + tracing::warn!( + "ignoring {field}: expected a string; found {}", + json_value_type(value) + ); + None + } + } +} + fn resolve_manifest_path( plugin_root: &Path, field: &'static str, diff --git a/codex-rs/core-plugins/src/marketplace_upgrade.rs b/codex-rs/core-plugins/src/marketplace_upgrade.rs index 9dda8a9a3e..010694c8b3 100644 --- a/codex-rs/core-plugins/src/marketplace_upgrade.rs +++ b/codex-rs/core-plugins/src/marketplace_upgrade.rs @@ -6,6 +6,7 @@ use self::activation::installed_marketplace_metadata_matches; use self::activation::write_installed_marketplace_metadata; use self::git::clone_git_source; use self::git::git_remote_revision; +use crate::marketplace::find_marketplace_manifest_path; use crate::marketplace::validate_marketplace_root; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerStack; @@ -176,9 +177,7 @@ fn upgrade_configured_git_marketplace( MARKETPLACE_UPGRADE_GIT_TIMEOUT, )?; let destination = install_root.join(&marketplace.name); - if destination - .join(".agents/plugins/marketplace.json") - .is_file() + if find_marketplace_manifest_path(&destination).is_some() && marketplace.last_revision.as_deref() == Some(remote_revision.as_str()) && installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision) { diff --git a/codex-rs/core-skills/src/injection.rs b/codex-rs/core-skills/src/injection.rs index df62f42e85..45979fa5c4 100644 --- a/codex-rs/core-skills/src/injection.rs +++ b/codex-rs/core-skills/src/injection.rs @@ -132,7 +132,7 @@ pub fn collect_explicit_skill_mentions( let mut blocked_plain_names: HashSet = HashSet::new(); for input in inputs { - if let UserInput::Skill { name, path } = input { + if let UserInput::Skill { name, path, .. } = input { blocked_plain_names.insert(name.clone()); let Ok(path) = AbsolutePathBuf::relative_to_current_dir(path) else { continue; diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 00d1cbba14..d8b05c604f 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -235,6 +235,7 @@ pub(crate) async fn skill_roots( config_layer_stack: &ConfigLayerStack, cwd: &AbsolutePathBuf, plugin_skill_roots: Vec, + extra_skill_roots: Vec, ) -> Vec { let home_dir = home_dir().and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok()); @@ -244,6 +245,7 @@ pub(crate) async fn skill_roots( cwd, home_dir.as_ref(), plugin_skill_roots, + extra_skill_roots, ) .await } @@ -254,6 +256,7 @@ async fn skill_roots_with_home_dir( cwd: &AbsolutePathBuf, home_dir: Option<&AbsolutePathBuf>, plugin_skill_roots: Vec, + extra_skill_roots: Vec, ) -> Vec { let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir, fs.clone()); roots.extend(plugin_skill_roots.into_iter().map(|root| SkillRoot { @@ -263,6 +266,13 @@ async fn skill_roots_with_home_dir( plugin_id: Some(root.plugin_id), plugin_root: Some(root.plugin_root), })); + roots.extend(extra_skill_roots.into_iter().map(|path| SkillRoot { + path, + scope: SkillScope::User, + file_system: Arc::clone(&LOCAL_FS), + plugin_id: None, + plugin_root: None, + })); roots.extend(repo_agents_skill_roots(fs, config_layer_stack, cwd).await); dedupe_skill_roots_by_path(&mut roots); roots @@ -339,6 +349,7 @@ fn skill_roots_from_layer_stack_inner( }); } ConfigLayerSource::Mdm { .. } + | ConfigLayerSource::EnterpriseManaged { .. } | ConfigLayerSource::SessionFlags | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {} @@ -1051,7 +1062,15 @@ pub(crate) async fn skill_roots_from_layer_stack( cwd: &AbsolutePathBuf, home_dir: Option<&AbsolutePathBuf>, ) -> Vec { - skill_roots_with_home_dir(Some(fs), config_layer_stack, cwd, home_dir, Vec::new()).await + skill_roots_with_home_dir( + Some(fs), + config_layer_stack, + cwd, + home_dir, + Vec::new(), + Vec::new(), + ) + .await } #[cfg(test)] diff --git a/codex-rs/core-skills/src/loader_tests.rs b/codex-rs/core-skills/src/loader_tests.rs index 84867ca66b..69e357d93d 100644 --- a/codex-rs/core-skills/src/loader_tests.rs +++ b/codex-rs/core-skills/src/loader_tests.rs @@ -1900,6 +1900,7 @@ async fn skill_roots_include_admin_with_lowest_priority() { &cfg.config_layer_stack, &cfg.cwd, Vec::new(), + Vec::new(), ) .await .into_iter() diff --git a/codex-rs/core-skills/src/manager.rs b/codex-rs/core-skills/src/manager.rs index db19322acf..185eaa9320 100644 --- a/codex-rs/core-skills/src/manager.rs +++ b/codex-rs/core-skills/src/manager.rs @@ -51,6 +51,7 @@ impl SkillsLoadInput { pub struct SkillsManager { codex_home: AbsolutePathBuf, restriction_product: Option, + extra_roots: RwLock>, cache_by_cwd: RwLock>, cache_by_config: RwLock>, } @@ -68,6 +69,7 @@ impl SkillsManager { let manager = Self { codex_home, restriction_product, + extra_roots: RwLock::new(Vec::new()), cache_by_cwd: RwLock::new(HashMap::new()), cache_by_config: RwLock::new(HashMap::new()), }; @@ -81,6 +83,17 @@ impl SkillsManager { manager } + pub fn set_extra_roots(&self, extra_roots: Vec) { + { + let mut roots = self + .extra_roots + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *roots = extra_roots; + } + self.clear_cache(); + } + /// Load skills for an already-constructed [`Config`], avoiding any additional config-layer /// loading. /// @@ -118,6 +131,7 @@ impl SkillsManager { &input.config_layer_stack, &input.cwd, input.effective_skill_roots.clone(), + self.extra_roots(), ) .await; if !input.bundled_skills_enabled { @@ -145,6 +159,7 @@ impl SkillsManager { &input.config_layer_stack, &input.cwd, input.effective_skill_roots.clone(), + self.extra_roots(), ) .await; if !bundled_skills_enabled_from_stack(&input.config_layer_stack) { @@ -214,6 +229,13 @@ impl SkillsManager { Err(err) => err.into_inner().get(cache_key).cloned(), } } + + fn extra_roots(&self) -> Vec { + match self.extra_roots.read() { + Ok(roots) => roots.clone(), + Err(err) => err.into_inner().clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/codex-rs/core-skills/src/manager_tests.rs b/codex-rs/core-skills/src/manager_tests.rs index 4afa83e21e..713b94d7b1 100644 --- a/codex-rs/core-skills/src/manager_tests.rs +++ b/codex-rs/core-skills/src/manager_tests.rs @@ -221,6 +221,128 @@ async fn skills_for_config_reuses_cache_for_same_effective_config() { assert_eq!(outcome2.skills, outcome1.skills); } +#[tokio::test] +async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() { + let codex_home = tempfile::tempdir().expect("tempdir"); + let cwd = tempfile::tempdir().expect("tempdir"); + let extra_root = tempfile::tempdir().expect("tempdir"); + let config_layer_stack = config_stack(&codex_home, ""); + let skills_manager = SkillsManager::new( + codex_home.path().abs(), + /*bundled_skills_enabled*/ true, + ); + + let skills_input = SkillsLoadInput::new( + cwd.path().abs(), + Vec::new(), + config_layer_stack.clone(), + bundled_skills_enabled_from_stack(&config_layer_stack), + ); + let empty_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert!( + empty_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + fs::create_dir_all(&skill_dir).expect("create skill dir"); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + ) + .expect("write skill"); + skills_manager.set_extra_roots(vec![extra_skills_root.abs()]); + + let runtime_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert!( + runtime_outcome + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + skills_manager.set_extra_roots(vec![extra_root.path().join("missing-skills").abs()]); + let replaced_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert_eq!(replaced_outcome.errors, Vec::new()); + assert!( + replaced_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); +} + +#[tokio::test] +async fn set_extra_roots_applies_to_config_loads_and_empty_clears() { + let codex_home = tempfile::tempdir().expect("tempdir"); + let cwd = tempfile::tempdir().expect("tempdir"); + let extra_root = tempfile::tempdir().expect("tempdir"); + let config_layer_stack = config_stack(&codex_home, ""); + let skills_manager = SkillsManager::new( + codex_home.path().abs(), + /*bundled_skills_enabled*/ true, + ); + + let empty_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + empty_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + fs::create_dir_all(&skill_dir).expect("create skill dir"); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + ) + .expect("write skill"); + skills_manager.set_extra_roots(vec![extra_skills_root.abs()]); + + let runtime_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + runtime_outcome + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + skills_manager.set_extra_roots(Vec::new()); + let cleared_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + cleared_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); +} + #[tokio::test] async fn skills_for_config_disables_plugin_skills_by_name() { let codex_home = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 361e958e86..478699c0a1 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -17,10 +17,6 @@ codex_rust_crate( # that relies on env!("CARGO_MANIFEST_DIR"). "CARGO_MANIFEST_DIR": "codex-rs/core", }, - integration_compile_data_extra = [ - "//codex-rs/apply-patch:apply_patch_tool_instructions.md", - "templates/realtime/backend_prompt.md", - ], integration_test_timeout = "long", test_data_extra = [ "config.schema.json", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 3960930328..881e440b7f 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -55,6 +55,7 @@ codex-plugin = { workspace = true } codex-model-provider = { workspace = true } codex-protocol = { workspace = true } codex-response-debug-context = { workspace = true } +codex-prompts = { workspace = true } codex-rollout = { workspace = true } codex-rollout-trace = { workspace = true } codex-rmcp-client = { workspace = true } @@ -73,7 +74,6 @@ codex-utils-plugins = { workspace = true } codex-utils-pty = { workspace = true } codex-utils-string = { workspace = true } codex-utils-stream-parser = { workspace = true } -codex-utils-template = { workspace = true } codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" } csv = { workspace = true } dirs = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index f6e15c8063..6dd85dffc2 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -473,6 +473,9 @@ "image_generation": { "type": "boolean" }, + "imagegenext": { + "type": "boolean" + }, "in_app_browser": { "type": "boolean" }, @@ -482,6 +485,9 @@ "js_repl_tools_only": { "type": "boolean" }, + "local_thread_store_compression": { + "type": "boolean" + }, "memories": { "type": "boolean" }, @@ -575,6 +581,9 @@ "sqlite": { "type": "boolean" }, + "standalone_web_search": { + "type": "boolean" + }, "steer": { "type": "boolean" }, @@ -608,6 +617,9 @@ "unified_exec": { "type": "boolean" }, + "unified_exec_zsh_fork": { + "type": "boolean" + }, "use_legacy_landlock": { "type": "boolean" }, @@ -757,6 +769,16 @@ }, "type": "object" }, + "ExperimentalRequestUserInput": { + "additionalProperties": false, + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, "ExternalConfigMigrationPrompts": { "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.", @@ -1767,7 +1789,7 @@ "NetworkProxyUnixSocketPermissionToml": { "enum": [ "allow", - "none" + "deny" ], "type": "string" }, @@ -1819,7 +1841,7 @@ "NetworkUnixSocketPermissionToml": { "enum": [ "allow", - "none" + "deny" ], "type": "string" }, @@ -2706,6 +2728,9 @@ "ToolsToml": { "additionalProperties": false, "properties": { + "experimental_request_user_input": { + "$ref": "#/definitions/ExperimentalRequestUserInput" + }, "web_search": { "allOf": [ { @@ -2763,7 +2788,8 @@ "chat": { "decrease_reasoning_effort": null, "edit_queued_message": null, - "increase_reasoning_effort": null + "increase_reasoning_effort": null, + "interrupt_turn": null }, "composer": { "history_search_next": null, @@ -2831,6 +2857,7 @@ "append_after_cursor": null, "append_line_end": null, "cancel_operator": null, + "change_to_line_end": null, "delete_char": null, "delete_to_line_end": null, "enter_insert": null, @@ -2847,8 +2874,10 @@ "open_line_above": null, "open_line_below": null, "paste_after": null, + "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, + "substitute_char": null, "yank_line": null }, "vim_operator": { @@ -2863,7 +2892,20 @@ "motion_word_backward": null, "motion_word_end": null, "motion_word_forward": null, + "select_around_text_object": null, + "select_inner_text_object": null, "yank_line": null + }, + "vim_text_object": { + "backtick": null, + "big_word": null, + "braces": null, + "brackets": null, + "cancel": null, + "double_quote": null, + "parentheses": null, + "single_quote": null, + "word": null } }, "description": "Keybinding overrides for the TUI.\n\nThis supports rebinding selected actions globally and by context. Context bindings take precedence over `global` bindings." @@ -3076,6 +3118,14 @@ } ], "description": "Increase the active reasoning effort." + }, + "interrupt_turn": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Interrupt the active turn." } }, "type": "object" @@ -3387,7 +3437,8 @@ "default": { "decrease_reasoning_effort": null, "edit_queued_message": null, - "increase_reasoning_effort": null + "increase_reasoning_effort": null, + "interrupt_turn": null } }, "composer": { @@ -3497,6 +3548,7 @@ "append_after_cursor": null, "append_line_end": null, "cancel_operator": null, + "change_to_line_end": null, "delete_char": null, "delete_to_line_end": null, "enter_insert": null, @@ -3513,8 +3565,10 @@ "open_line_above": null, "open_line_below": null, "paste_after": null, + "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, + "substitute_char": null, "yank_line": null } }, @@ -3536,8 +3590,28 @@ "motion_word_backward": null, "motion_word_end": null, "motion_word_forward": null, + "select_around_text_object": null, + "select_inner_text_object": null, "yank_line": null } + }, + "vim_text_object": { + "allOf": [ + { + "$ref": "#/definitions/TuiVimTextObjectKeymap" + } + ], + "default": { + "backtick": null, + "big_word": null, + "braces": null, + "brackets": null, + "cancel": null, + "double_quote": null, + "parentheses": null, + "single_quote": null, + "word": null + } } }, "type": "object" @@ -3762,6 +3836,14 @@ ], "description": "Cancel a pending operator and return to normal mode." }, + "change_to_line_end": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Change from cursor to end of line and enter insert mode (`C`)." + }, "delete_char": { "allOf": [ { @@ -3890,6 +3972,14 @@ ], "description": "Paste after cursor (`p`)." }, + "start_change_operator": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Begin change operator; next keys select a text object." + }, "start_delete_operator": { "allOf": [ { @@ -3906,6 +3996,14 @@ ], "description": "Begin yank operator; next key selects motion (`y`)." }, + "substitute_char": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Delete character under cursor and enter insert mode (`s`)." + }, "yank_line": { "allOf": [ { @@ -4009,6 +4107,22 @@ ], "description": "Motion: to start of next word (`w`)." }, + "select_around_text_object": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Select an around text object after an operator." + }, + "select_inner_text_object": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Select an inner text object after an operator." + }, "yank_line": { "allOf": [ { @@ -4020,6 +4134,85 @@ }, "type": "object" }, + "TuiVimTextObjectKeymap": { + "additionalProperties": false, + "description": "Vim text-object keybindings for modal editing inside text areas.", + "properties": { + "backtick": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: backticks." + }, + "big_word": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: whitespace-delimited WORD." + }, + "braces": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: braces." + }, + "brackets": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: brackets." + }, + "cancel": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Cancel the pending text-object command." + }, + "double_quote": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: double quotes." + }, + "parentheses": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: parentheses." + }, + "single_quote": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: single quotes." + }, + "word": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: word." + } + }, + "type": "object" + }, "UriBasedFileOpener": { "oneOf": [ { @@ -4403,6 +4596,9 @@ "image_generation": { "type": "boolean" }, + "imagegenext": { + "type": "boolean" + }, "in_app_browser": { "type": "boolean" }, @@ -4412,6 +4608,9 @@ "js_repl_tools_only": { "type": "boolean" }, + "local_thread_store_compression": { + "type": "boolean" + }, "memories": { "type": "boolean" }, @@ -4505,6 +4704,9 @@ "sqlite": { "type": "boolean" }, + "standalone_web_search": { + "type": "boolean" + }, "steer": { "type": "boolean" }, @@ -4538,6 +4740,9 @@ "unified_exec": { "type": "boolean" }, + "unified_exec_zsh_fork": { + "type": "boolean" + }, "use_legacy_landlock": { "type": "boolean" }, diff --git a/codex-rs/core/src/agent/agent_resolver.rs b/codex-rs/core/src/agent/agent_resolver.rs index eb806da3ae..115739c2fd 100644 --- a/codex-rs/core/src/agent/agent_resolver.rs +++ b/codex-rs/core/src/agent/agent_resolver.rs @@ -32,5 +32,5 @@ fn register_session_root(session: &Arc, turn: &Arc) { session .services .agent_control - .register_session_root(session.conversation_id, &turn.session_source); + .register_session_root(session.conversation_id, turn.parent_thread_id); } diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index a9a90be54a..eecc46d587 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -54,6 +54,7 @@ pub(crate) enum SpawnAgentForkMode { pub(crate) struct SpawnAgentOptions { pub(crate) fork_parent_spawn_call_id: Option, pub(crate) fork_mode: Option, + pub(crate) parent_thread_id: Option, pub(crate) environments: Option>, } @@ -266,8 +267,9 @@ impl AgentControl { config.clone(), self.clone(), session_source, + options.parent_thread_id, + /*forked_from_thread_id*/ None, /*thread_source*/ Some(ThreadSource::Subagent), - /*persist_extended_history*/ false, /*metrics_service_name*/ None, inherited_shell_snapshot, inherited_exec_policy, @@ -307,6 +309,7 @@ impl AgentControl { } }; let thread_config = new_thread.thread.codex.thread_config_snapshot().await; + let parent_thread_id = thread_config.parent_thread_id; emit_subagent_session_started( &new_thread .thread @@ -315,8 +318,9 @@ impl AgentControl { .services .analytics_events_client, client_metadata, + new_thread.thread.codex.session.session_id(), new_thread.thread_id, - /*parent_thread_id*/ None, + parent_thread_id, thread_config, subagent_source.clone(), ); @@ -470,6 +474,7 @@ impl AgentControl { } if preserve_reference_context_item && config.features.enabled(Feature::MultiAgentV2) + && config.multi_agent_v2.usage_hint_enabled && let Some(subagent_usage_hint_text) = config.multi_agent_v2.subagent_usage_hint_text.clone() && let Some(subagent_usage_hint_message) = @@ -487,7 +492,8 @@ impl AgentControl { self.clone(), session_source, /*thread_source*/ Some(ThreadSource::Subagent), - /*persist_extended_history*/ false, + /*parent_thread_id*/ Some(parent_thread_id), + /*forked_from_thread_id*/ Some(parent_thread_id), inherited_shell_snapshot, inherited_exec_policy, options.environments.clone(), @@ -634,6 +640,7 @@ impl AgentControl { .history .ok_or_else(|| CodexErr::ThreadNotFound(thread_id))? .items; + let parent_thread_id = stored_thread.parent_thread_id; let resumed_thread = state .resume_thread_with_history_with_source(ResumeThreadWithHistoryOptions { @@ -645,6 +652,7 @@ impl AgentControl { }), agent_control: self.clone(), session_source, + parent_thread_id, inherited_shell_snapshot, inherited_exec_policy, }) @@ -700,22 +708,6 @@ impl AgentControl { result } - /// Append a prebuilt message to an existing agent thread outside the normal user-input path. - #[cfg(test)] - pub(crate) async fn append_message( - &self, - agent_id: ThreadId, - message: ResponseItem, - ) -> CodexResult { - let state = self.upgrade()?; - self.handle_thread_request_result( - agent_id, - &state, - state.append_message(agent_id, message).await, - ) - .await - } - pub(crate) async fn send_inter_agent_communication( &self, agent_id: ThreadId, @@ -784,15 +776,45 @@ impl AgentControl { /// agent and any live descendants reached from the in-memory tree. pub(crate) async fn close_agent(&self, agent_id: ThreadId) -> CodexResult { let state = self.upgrade()?; - if let Ok(thread) = state.get_thread(agent_id).await - && let Some(state_db_ctx) = thread.state_db() - && let Err(err) = state_db_ctx - .set_thread_spawn_edge_status(agent_id, DirectionalThreadSpawnEdgeStatus::Closed) - .await - { - warn!("failed to persist thread-spawn edge status for {agent_id}: {err}"); + let known_agent = self.state.agent_metadata_for_thread(agent_id).is_some(); + match state.get_thread(agent_id).await { + Ok(thread) => { + if let Some(state_db_ctx) = thread.state_db() + && let Err(err) = state_db_ctx + .set_thread_spawn_edge_status( + agent_id, + DirectionalThreadSpawnEdgeStatus::Closed, + ) + .await + { + warn!("failed to persist thread-spawn edge status for {agent_id}: {err}"); + } + } + Err(CodexErr::ThreadNotFound(_)) if known_agent => { + if let Some(state_db_ctx) = state.state_db() + && let Err(err) = state_db_ctx + .set_thread_spawn_edge_status( + agent_id, + DirectionalThreadSpawnEdgeStatus::Closed, + ) + .await + { + return Err(CodexErr::Fatal(format!( + "failed to persist stale thread-spawn edge status for {agent_id}: {err}" + ))); + } + } + Err(CodexErr::ThreadNotFound(_)) => {} + Err(err) => { + warn!("failed to inspect agent before close {agent_id}: {err}"); + } + } + match Box::pin(self.shutdown_agent_tree(agent_id)).await { + Err(CodexErr::ThreadNotFound(_)) | Err(CodexErr::InternalAgentDied) if known_agent => { + Ok(String::new()) + } + result => result, } - Box::pin(self.shutdown_agent_tree(agent_id)).await } /// Shut down `agent_id` and any live descendants reachable from the in-memory spawn tree. @@ -823,9 +845,9 @@ impl AgentControl { pub(crate) fn register_session_root( &self, current_thread_id: ThreadId, - current_session_source: &SessionSource, + current_parent_thread_id: Option, ) { - if thread_spawn_parent_thread_id(current_session_source).is_none() { + if current_parent_thread_id.is_none() { self.state.register_root_thread(current_thread_id); } } @@ -1200,7 +1222,8 @@ impl AgentControl { child_thread_id: ThreadId, session_source: Option<&SessionSource>, ) { - let Some(parent_thread_id) = session_source.and_then(thread_spawn_parent_thread_id) else { + let Some(parent_thread_id) = session_source.and_then(SessionSource::parent_thread_id) + else { return; }; let Some(state_db_ctx) = thread.state_db() else { @@ -1245,15 +1268,6 @@ impl AgentControl { } } -fn thread_spawn_parent_thread_id(session_source: &SessionSource) -> Option { - match session_source { - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id, .. - }) => Some(*parent_thread_id), - _ => None, - } -} - fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> bool { if prefix.is_root() { return true; @@ -1278,8 +1292,10 @@ pub(crate) fn render_input_preview(initial_operation: &Op) -> String { UserInput::LocalImage { path, .. } => { format!("[local_image:{}]", path.display()) } - UserInput::Skill { name, path } => format!("[skill:${name}]({})", path.display()), - UserInput::Mention { name, path } => format!("[mention:${name}]({path})"), + UserInput::Skill { name, path, .. } => { + format!("[skill:${name}]({})", path.display()) + } + UserInput::Mention { name, path, .. } => format!("[mention:${name}]({path})"), _ => "[input]".to_string(), }) .collect::>() diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index ed39df1548..3e0242c037 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -74,6 +74,15 @@ fn assistant_message(text: &str, phase: Option) -> ResponseItem { } } +#[test] +fn register_session_root_skips_threads_with_explicit_parent() { + let control = AgentControl::default(); + + control.register_session_root(ThreadId::new(), Some(ThreadId::new())); + + assert_eq!(control.state.agent_id_for_path(&AgentPath::root()), None); +} + fn spawn_agent_call(call_id: &str) -> ResponseItem { ResponseItem::FunctionCall { id: None, @@ -444,6 +453,7 @@ async fn send_input_submits_user_message() { }], final_output_json_schema: None, responsesapi_client_metadata: None, + additional_context: Default::default(), thread_settings: Default::default(), }, ); @@ -517,60 +527,6 @@ async fn send_inter_agent_communication_without_turn_queues_message_without_trig )); } -#[tokio::test] -async fn append_message_records_assistant_message() { - let harness = AgentControlHarness::new().await; - let (thread_id, thread) = harness.start_thread().await; - let message = - "author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: hello from tests"; - - let submission_id = harness - .control - .append_message( - thread_id, - ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![ContentItem::InputText { - text: message.to_string(), - }], - phase: None, - }, - ) - .await - .expect("append_message should succeed"); - assert!(!submission_id.is_empty()); - - timeout(Duration::from_secs(5), async { - loop { - let history_items = thread - .codex - .session - .clone_history() - .await - .raw_items() - .to_vec(); - let recorded = history_items.iter().any(|item| { - matches!( - item, - ResponseItem::Message { role, content, .. } - if role == "assistant" - && content.iter().any(|content_item| matches!( - content_item, - ContentItem::InputText { text } if text == message - )) - ) - }); - if recorded { - break; - } - sleep(Duration::from_millis(10)).await; - } - }) - .await - .expect("assistant message should be recorded"); -} - #[tokio::test] async fn spawn_agent_creates_thread_and_sends_prompt() { let harness = AgentControlHarness::new().await; @@ -598,6 +554,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() { }], final_output_json_schema: None, responsesapi_client_metadata: None, + additional_context: Default::default(), thread_settings: Default::default(), }, ); @@ -760,6 +717,53 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { "full-history forked child should preserve the parent diff baseline" ); + let mut disabled_hint_child_config = harness.config.clone(); + let _ = disabled_hint_child_config + .features + .enable(Feature::MultiAgentV2); + disabled_hint_child_config.multi_agent_v2.usage_hint_enabled = false; + disabled_hint_child_config + .multi_agent_v2 + .subagent_usage_hint_text = Some("Disabled child subagent guidance.".to_string()); + let disabled_hint_child_thread_id = harness + .control + .spawn_agent_with_metadata( + disabled_hint_child_config, + text_input("child task without hints"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), + fork_mode: Some(SpawnAgentForkMode::FullHistory), + ..Default::default() + }, + ) + .await + .expect("forked spawn should honor disabled usage hints") + .thread_id; + let disabled_hint_child_thread = harness + .manager + .get_thread(disabled_hint_child_thread_id) + .await + .expect("disabled-hint child thread should be registered"); + let disabled_hint_history = disabled_hint_child_thread + .codex + .session + .clone_history() + .await; + assert!( + !history_contains_text( + disabled_hint_history.raw_items(), + "Disabled child subagent guidance.", + ), + "full-history forked child should not add subagent guidance when usage hints are disabled" + ); + let expected = ( child_thread_id, Op::UserInput { @@ -770,6 +774,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { }], final_output_json_schema: None, responsesapi_client_metadata: None, + additional_context: Default::default(), thread_settings: Default::default(), }, ); @@ -785,6 +790,11 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .shutdown_live_agent(child_thread_id) .await .expect("child shutdown should submit"); + let _ = harness + .control + .shutdown_live_agent(disabled_hint_child_thread_id) + .await + .expect("disabled-hint child shutdown should submit"); let _ = parent_thread .submit(Op::Shutdown {}) .await @@ -1110,6 +1120,108 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() { .expect("parent shutdown should submit"); } +#[tokio::test] +async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_limit() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, parent_thread) = harness.start_thread().await; + let startup_turn_context = parent_thread.codex.session.new_default_turn().await; + parent_thread + .codex + .session + .record_conversation_items( + startup_turn_context.as_ref(), + &[ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: "parent startup developer context".to_string(), + }], + phase: None, + }], + ) + .await; + parent_thread + .inject_user_message_without_turn("current parent task".to_string()) + .await; + let spawn_turn_context = parent_thread.codex.session.new_default_turn().await; + let parent_spawn_call_id = "spawn-call-last-n-under-limit".to_string(); + parent_thread + .codex + .session + .record_conversation_items( + spawn_turn_context.as_ref(), + &[spawn_agent_call(&parent_spawn_call_id)], + ) + .await; + parent_thread + .codex + .session + .ensure_rollout_materialized() + .await; + parent_thread + .codex + .session + .flush_rollout() + .await + .expect("parent rollout should flush"); + + let child_thread_id = harness + .control + .spawn_agent_with_metadata( + harness.config.clone(), + text_input("child task"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id), + fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)), + ..Default::default() + }, + ) + .await + .expect("bounded forked spawn should drop startup prefix") + .thread_id; + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + let history = child_thread.codex.session.clone_history().await; + assert!( + history_contains_text(history.raw_items(), "current parent task"), + "bounded fork should retain the requested recent parent turn" + ); + assert!( + !history_contains_text(history.raw_items(), "parent startup developer context"), + "bounded fork should drop parent startup context even when fewer turns exist than requested" + ); + assert!( + child_thread + .codex + .session + .reference_context_item() + .await + .is_none(), + "bounded forked child should still rebuild context after truncating the cached prefix" + ); + + let _ = harness + .control + .shutdown_live_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + let _ = parent_thread + .submit(Op::Shutdown {}) + .await + .expect("parent shutdown should submit"); +} + #[tokio::test] async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() { let harness = AgentControlHarness::new().await; diff --git a/codex-rs/core/src/agents_md.rs b/codex-rs/core/src/agents_md.rs index 6daaff4a40..8048552480 100644 --- a/codex-rs/core/src/agents_md.rs +++ b/codex-rs/core/src/agents_md.rs @@ -25,15 +25,13 @@ use codex_exec_server::Environment; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; use codex_features::Feature; +use codex_prompts::HIERARCHICAL_AGENTS_MESSAGE; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize as normalize_path; use std::io; use toml::Value as TomlValue; use tracing::error; -pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str = - include_str!("../hierarchical_agents_message.md"); - /// Default filename scanned for AGENTS.md instructions. pub const DEFAULT_AGENTS_MD_FILENAME: &str = "AGENTS.md"; /// Preferred local override for AGENTS.md instructions. diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d84b32a8b4..124dfb18c0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -173,6 +173,7 @@ struct ModelClientState { provider: SharedModelProvider, auth_env_telemetry: AuthEnvTelemetry, session_source: SessionSource, + parent_thread_id: Option, model_verbosity: Option, enable_request_compression: bool, include_timing_metrics: bool, @@ -218,6 +219,7 @@ impl RequestRouteTelemetry { #[derive(Debug, Clone)] pub struct ModelClient { state: Arc, + prompt_cache_key_override: Option, } /// A turn-scoped streaming session created from a [`ModelClient`]. @@ -320,6 +322,7 @@ impl ModelClient { installation_id: String, provider_info: ModelProviderInfo, session_source: SessionSource, + parent_thread_id: Option, model_verbosity: Option, enable_request_compression: bool, include_timing_metrics: bool, @@ -343,6 +346,7 @@ impl ModelClient { provider: model_provider, auth_env_telemetry, session_source, + parent_thread_id, model_verbosity, enable_request_compression, include_timing_metrics, @@ -352,9 +356,24 @@ impl ModelClient { disable_websockets: AtomicBool::new(false), cached_websocket_session: StdMutex::new(WebsocketSession::default()), }), + prompt_cache_key_override: None, } } + pub(crate) fn with_prompt_cache_key_override( + mut self, + prompt_cache_key_override: Option, + ) -> Self { + self.prompt_cache_key_override = prompt_cache_key_override; + self + } + + fn prompt_cache_key(&self) -> String { + self.prompt_cache_key_override + .clone() + .unwrap_or_else(|| self.state.thread_id.to_string()) + } + /// Creates a fresh turn-scoped streaming session. /// /// This constructor does not perform network I/O itself; the session opens a websocket lazily @@ -383,7 +402,7 @@ impl ModelClient { self.store_cached_websocket_session(WebsocketSession::default()); } - fn current_window_id(&self) -> String { + pub(crate) fn current_window_id(&self) -> String { let thread_id = self.state.thread_id; let window_generation = self.state.window_generation.load(Ordering::Relaxed); format!("{thread_id}:{window_generation}") @@ -441,6 +460,7 @@ impl ModelClient { settings: CompactConversationRequestSettings, session_telemetry: &SessionTelemetry, compaction_trace: &CompactionTraceContext, + turn_metadata_header: Option<&str>, ) -> Result> { if prompt.input.is_empty() { return Ok(Vec::new()); @@ -496,7 +516,7 @@ impl ModelClient { extra_headers.extend(build_responses_headers( self.state.beta_features_header.as_deref(), /*turn_state*/ None, - /*turn_metadata_header*/ None, + parse_turn_metadata_header(turn_metadata_header).as_ref(), )); extra_headers.extend(self.build_responses_identity_headers()); extra_headers.extend(build_session_headers( @@ -620,7 +640,7 @@ impl ModelClient { fn build_responses_identity_headers(&self) -> ApiHeaderMap { let mut extra_headers = self.build_subagent_headers(); - if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source) + if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id) && let Ok(val) = HeaderValue::from_str(&parent_thread_id) { extra_headers.insert(X_CODEX_PARENT_THREAD_ID_HEADER, val); @@ -647,7 +667,7 @@ impl ModelClient { if let Some(subagent) = subagent_header_value(&self.state.session_source) { client_metadata.insert(X_OPENAI_SUBAGENT_HEADER.to_string(), subagent); } - if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source) { + if let Some(parent_thread_id) = parent_thread_id_header_value(self.state.parent_thread_id) { client_metadata.insert( X_CODEX_PARENT_THREAD_ID_HEADER.to_string(), parent_thread_id, @@ -748,7 +768,7 @@ impl ModelClient { &prompt.output_schema, prompt.output_schema_strict, ); - let prompt_cache_key = Some(self.state.thread_id.to_string()); + let prompt_cache_key = Some(self.prompt_cache_key()); let service_tier = model_info.service_tier_for_request(service_tier); let request = ResponsesApiRequest { model: model_info.slug.clone(), @@ -1716,20 +1736,8 @@ fn subagent_header_value(session_source: &SessionSource) -> Option { } } -fn parent_thread_id_header_value(session_source: &SessionSource) -> Option { - match session_source { - SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id, .. - }) => Some(parent_thread_id.to_string()), - SessionSource::Cli - | SessionSource::VSCode - | SessionSource::Exec - | SessionSource::Mcp - | SessionSource::Custom(_) - | SessionSource::Internal(_) - | SessionSource::SubAgent(_) - | SessionSource::Unknown => None, - } +fn parent_thread_id_header_value(parent_thread_id: Option) -> Option { + parent_thread_id.map(|parent_thread_id| parent_thread_id.to_string()) } const RESPONSE_STREAM_CHANNEL_CAPACITY: usize = 1600; diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index d9edadbed4..d775f9c01c 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -12,14 +12,6 @@ use std::task::Poll; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -/// Review thread system prompt. Edit `core/src/review_prompt.md` to customize. -pub const REVIEW_PROMPT: &str = include_str!("../review_prompt.md"); - -// Centralized templates for review-related user messages -pub const REVIEW_EXIT_SUCCESS_TMPL: &str = include_str!("../templates/review/exit_success.xml"); -pub const REVIEW_EXIT_INTERRUPTED_TMPL: &str = - include_str!("../templates/review/exit_interrupted.xml"); - /// API request payload for a single model turn #[derive(Debug, Clone)] pub struct Prompt { diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index b9d9172c83..f8d904036a 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -61,6 +61,13 @@ use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt; fn test_model_client(session_source: SessionSource) -> ModelClient { + test_model_client_with_parent(session_source, /*parent_thread_id*/ None) +} + +fn test_model_client_with_parent( + session_source: SessionSource, + parent_thread_id: Option, +) -> ModelClient { let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses); let thread_id = ThreadId::new(); ModelClient::new( @@ -70,6 +77,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient { /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), provider, session_source, + parent_thread_id, /*model_verbosity*/ None, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, @@ -272,13 +280,16 @@ fn build_subagent_headers_sets_internal_memory_consolidation_label() { #[test] fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() { let parent_thread_id = ThreadId::new(); - let client = test_model_client(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { - parent_thread_id, - depth: 2, - agent_path: None, - agent_nickname: None, - agent_role: None, - })); + let client = test_model_client_with_parent( + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 2, + agent_path: None, + agent_nickname: None, + agent_role: None, + }), + Some(parent_thread_id), + ); client.advance_window_generation(); @@ -520,6 +531,7 @@ fn model_client_with_counting_attestation( /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), provider, SessionSource::Exec, + /*parent_thread_id*/ None, /*model_verbosity*/ None, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 4ed6b2368c..74854b4005 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -74,6 +74,8 @@ pub(crate) async fn run_codex_thread_interactive( ) -> Result { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_ops, rx_ops) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); + let conversation_history = initial_history.unwrap_or(InitialHistory::New); + let forked_from_thread_id = conversation_history.forked_from_id(); let CodexSpawnOk { codex, .. } = Box::pin(Codex::spawn(CodexSpawnArgs { config, installation_id: parent_session.installation_id.clone(), @@ -84,12 +86,13 @@ pub(crate) async fn run_codex_thread_interactive( plugins_manager: Arc::clone(&parent_session.services.plugins_manager), mcp_manager: Arc::clone(&parent_session.services.mcp_manager), extensions: Arc::clone(&parent_session.services.extensions), - conversation_history: initial_history.unwrap_or(InitialHistory::New), + conversation_history, session_source: SessionSource::SubAgent(subagent_source.clone()), + forked_from_thread_id, + parent_thread_id: Some(parent_session.conversation_id), thread_source: Some(ThreadSource::Subagent), agent_control: parent_session.services.agent_control.clone(), dynamic_tools: Vec::new(), - persist_extended_history: false, metrics_service_name: None, inherited_shell_snapshot: None, user_shell_override: None, @@ -108,6 +111,7 @@ pub(crate) async fn run_codex_thread_interactive( emit_subagent_session_started( &parent_session.services.analytics_events_client, client_metadata, + codex.session.session_id(), codex.session.conversation_id, Some(parent_session.conversation_id), thread_config, @@ -192,6 +196,7 @@ pub(crate) async fn run_codex_thread_one_shot( items: input, final_output_json_schema, responsesapi_client_metadata: None, + additional_context: Default::default(), thread_settings: Default::default(), }) .await?; @@ -215,6 +220,7 @@ pub(crate) async fn run_codex_thread_one_shot( .send(Submission { id: "shutdown".to_string(), op: Op::Shutdown {}, + client_user_message_id: None, trace: None, }) .await; diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index 66cde8d1ea..04f6ae3965 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -129,6 +129,7 @@ async fn forward_ops_preserves_submission_trace_context() { let submission = Submission { id: "sub-1".to_string(), op: Op::Interrupt, + client_user_message_id: None, trace: Some(codex_protocol::protocol::W3cTraceContext { traceparent: Some( "00-1234567890abcdef1234567890abcdef-1234567890abcdef-01".to_string(), diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 4b77efdac9..7a6bc69c5c 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -7,6 +7,7 @@ use crate::session::SessionSettingsUpdate; use crate::session::SteerInputError; use codex_features::Feature; use codex_otel::SessionTelemetry; +use codex_protocol::ThreadId; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; @@ -18,9 +19,9 @@ use codex_protocol::mcp::CallToolResult; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ContentItem; use codex_protocol::models::PermissionProfile; -use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::protocol::AdditionalContextEntry; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::Op; @@ -41,6 +42,7 @@ use codex_thread_store::ThreadStoreError; use codex_thread_store::ThreadStoreResult; use codex_utils_absolute_path::AbsolutePathBuf; use rmcp::model::ReadResourceRequestParams; +use std::collections::BTreeMap; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -67,6 +69,7 @@ pub struct ThreadConfigSnapshot { pub personality: Option, pub collaboration_mode: CollaborationMode, pub session_source: SessionSource, + pub parent_thread_id: Option, pub thread_source: Option, } @@ -228,6 +231,17 @@ impl CodexThread { self.codex.submit_with_trace(op, trace).await } + pub async fn submit_user_input_with_client_user_message_id( + &self, + op: Op, + trace: Option, + client_user_message_id: Option, + ) -> CodexResult { + self.codex + .submit_user_input_with_client_user_message_id(op, trace, client_user_message_id) + .await + } + /// Persist whether this thread is eligible for future memory generation. pub async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()> { self.codex.set_thread_memory_mode(mode).await @@ -236,23 +250,40 @@ impl CodexThread { pub async fn steer_input( &self, input: Vec, + additional_context: BTreeMap, expected_turn_id: Option<&str>, + client_user_message_id: Option, responsesapi_client_metadata: Option>, ) -> Result { self.codex - .steer_input(input, expected_turn_id, responsesapi_client_metadata) + .steer_input( + input, + additional_context, + expected_turn_id, + client_user_message_id, + responsesapi_client_metadata, + ) .await } - /// Injects hidden model-visible items into the currently active turn. + /// Injects model-visible items into the currently active turn. /// - /// This is the runtime-owned counterpart to user-facing `steer_input`. + /// This is the thread-level bridge to `Session::inject_if_running` for + /// callers that only hold a `CodexThread`. /// It returns the unchanged items when this thread has no active turn. - pub async fn inject_response_items_into_active_turn( + pub async fn inject_if_running( &self, - items: Vec, - ) -> Result<(), Vec> { - self.codex.session.inject_response_items(items).await + items: Vec, + ) -> Result<(), Vec> { + self.codex.session.inject_if_running(items).await + } + + /// Starts a regular turn with model-visible items only if the thread is idle. + pub async fn try_start_turn_if_idle( + &self, + items: Vec, + ) -> Result<(), Vec> { + self.codex.session.try_start_turn_if_idle(items).await } pub async fn set_app_server_client_info( @@ -358,61 +389,19 @@ impl CodexThread { /// Records a user-role session-prefix message without creating a new user turn boundary. pub(crate) async fn inject_user_message_without_turn(&self, message: String) { - let message = ResponseItem::Message { + let item = ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: message }], phase: None, }; - let pending_item = match pending_message_input_item(&message) { - Ok(pending_item) => pending_item, - Err(err) => { - debug_assert!(false, "session-prefix message append should succeed: {err}"); - return; - } - }; - if self - .codex + self.codex .session - .inject_response_items(vec![pending_item]) - .await - .is_err() - { - let turn_context = self.codex.session.new_default_turn().await; - self.codex - .session - .record_conversation_items(turn_context.as_ref(), &[message]) - .await; - } + .inject_no_new_turn(vec![item], /*current_turn_context*/ None) + .await; } - /// Append a prebuilt message to the thread history without treating it as a user turn. - /// - /// If the thread already has an active turn, the message is queued as pending input for that - /// turn. Otherwise it is queued at session scope and a regular turn is started so the agent - /// can consume that pending input through the normal turn pipeline. - #[cfg(test)] - pub(crate) async fn append_message(&self, message: ResponseItem) -> CodexResult { - let submission_id = uuid::Uuid::new_v4().to_string(); - let pending_item = pending_message_input_item(&message)?; - if let Err(items) = self - .codex - .session - .inject_response_items(vec![pending_item]) - .await - { - self.codex - .session - .input_queue - .queue_response_items_for_next_turn(items) - .await; - self.codex.session.maybe_start_turn_for_pending_work().await; - } - - Ok(submission_id) - } - - /// Append raw Responses API items to the thread's model-visible history. + /// Record raw Responses API items without starting a new turn. pub async fn inject_response_items(&self, items: Vec) -> CodexResult<()> { if items.is_empty() { return Err(CodexErr::InvalidRequest( @@ -429,7 +418,7 @@ impl CodexThread { } self.codex .session - .record_conversation_items(turn_context.as_ref(), &items) + .inject_no_new_turn(items, Some(turn_context.as_ref())) .await; self.codex.session.flush_rollout().await?; Ok(()) @@ -532,13 +521,7 @@ impl CodexThread { let result = self .codex .session - .read_resource( - server, - ReadResourceRequestParams { - meta: None, - uri: uri.to_string(), - }, - ) + .read_resource(server, ReadResourceRequestParams::new(uri)) .await?; Ok(serde_json::to_value(result)?) @@ -596,21 +579,3 @@ impl CodexThread { Ok(*guard) } } - -fn pending_message_input_item(message: &ResponseItem) -> CodexResult { - match message { - ResponseItem::Message { - role, - content, - phase, - .. - } => Ok(ResponseInputItem::Message { - role: role.clone(), - content: content.clone(), - phase: phase.clone(), - }), - _ => Err(CodexErr::InvalidRequest( - "append_message only supports ResponseItem::Message".to_string(), - )), - } -} diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index f2bded8d06..11386e6bcd 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -13,6 +13,7 @@ use crate::session::PreviousTurnSettings; use crate::session::session::Session; use crate::session::turn::get_last_assistant_message_from_turn; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use crate::util::backoff; use codex_analytics::CodexCompactionEvent; use codex_analytics::CompactionImplementation; @@ -43,8 +44,8 @@ use tracing::error; use codex_model_provider_info::ModelProviderInfo; -pub const SUMMARIZATION_PROMPT: &str = include_str!("../templates/compact/prompt.md"); -pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_prefix.md"); +pub use codex_prompts::SUMMARIZATION_PROMPT; +pub use codex_prompts::SUMMARY_PREFIX; const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000; /// Controls whether compaction replacement history must include initial context. @@ -128,6 +129,8 @@ async fn run_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = + CompactionTurnMetadata::new(trigger, reason, CompactionImplementation::Responses, phase); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -153,6 +156,7 @@ async fn run_compact_task_inner( Arc::clone(&turn_context), input, initial_context_injection, + compaction_metadata, ) .await; let status = compaction_status_from_result(&result); @@ -173,6 +177,7 @@ async fn run_compact_task_inner_impl( turn_context: Arc, input: Vec, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(&turn_context, &compaction_item) @@ -204,7 +209,10 @@ async fn run_compact_task_inner_impl( personality: turn_context.personality, ..Default::default() }; - let turn_metadata_header = turn_context.turn_metadata_state.current_header_value(); + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let attempt_result = drain_to_completed( &sess, turn_context.as_ref(), @@ -560,7 +568,7 @@ async fn drain_to_completed( }; match event { Ok(ResponseEvent::OutputItemDone(item)) => { - sess.record_into_history(std::slice::from_ref(&item), turn_context) + sess.record_conversation_items(turn_context, std::slice::from_ref(&item)) .await; } Ok(ResponseEvent::ServerReasoningIncluded(included)) => { diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 969da47544..b4456dc717 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -17,6 +17,7 @@ use crate::hook_runtime::run_pre_compact_hooks; use crate::session::session::Session; use crate::session::turn::built_tools; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; @@ -89,6 +90,12 @@ async fn run_remote_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = CompactionTurnMetadata::new( + trigger, + reason, + CompactionImplementation::ResponsesCompact, + phase, + ); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -113,8 +120,13 @@ async fn run_remote_compact_task_inner( return Err(CodexErr::TurnAborted); } } - let result = - run_remote_compact_task_inner_impl(sess, turn_context, initial_context_injection).await; + let result = run_remote_compact_task_inner_impl( + sess, + turn_context, + initial_context_injection, + compaction_metadata, + ) + .await; let status = compaction_status_from_result(&result); let error = result.as_ref().err().map(ToString::to_string); if result.is_ok() { @@ -139,6 +151,7 @@ async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult<()> { let context_compaction_item = ContextCompactionItem::new(); // Use the UI compaction item ID as the trace compaction ID so protocol lifecycle events, @@ -186,6 +199,10 @@ async fn run_remote_compact_task_inner_impl( output_schema: None, output_schema_strict: true, }; + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let mut new_history = sess .services .model_client @@ -203,6 +220,7 @@ async fn run_remote_compact_task_inner_impl( }, &turn_context.session_telemetry, &compaction_trace, + turn_metadata_header.as_deref(), ) .or_else(|err| async { let total_usage_breakdown = sess.get_total_token_usage_breakdown().await; diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index dcf241f990..0da3017f7d 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -21,6 +21,7 @@ use crate::responses_retry::handle_retryable_response_stream_error; use crate::session::session::Session; use crate::session::turn::built_tools; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; @@ -105,6 +106,12 @@ async fn run_remote_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = CompactionTurnMetadata::new( + trigger, + reason, + CompactionImplementation::ResponsesCompactionV2, + phase, + ); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -134,6 +141,7 @@ async fn run_remote_compact_task_inner( turn_context, client_session, initial_context_injection, + compaction_metadata, ) .await; let status = compaction_status_from_result(&result); @@ -161,6 +169,7 @@ async fn run_remote_compact_task_inner_impl( turn_context: &Arc, client_session: Option<&mut ModelClientSession>, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult<()> { let context_compaction_item = ContextCompactionItem::new(); let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context( @@ -208,7 +217,10 @@ async fn run_remote_compact_task_inner_impl( output_schema_strict: true, }; - let turn_metadata_header = turn_context.turn_metadata_state.current_header_value(); + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let trace_attempt = compaction_trace.start_attempt(&serde_json::json!({ "model": turn_context.model_info.slug.as_str(), "instructions": prompt.base_instructions.text.as_str(), diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 2682e6ef41..d0097d7537 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1103,6 +1103,7 @@ allowed_approval_policies = ["on-request"] allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1164,6 +1165,7 @@ allowed_approval_policies = ["on-request"] allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1374,6 +1376,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e8866aab3a..70719843ba 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -12,6 +12,7 @@ use codex_config::config_toml::AgentRoleToml; use codex_config::config_toml::AgentsToml; use codex_config::config_toml::AutoReviewToml; use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ExperimentalRequestUserInput; use codex_config::config_toml::ProjectConfig; use codex_config::config_toml::RealtimeConfig; use codex_config::config_toml::RealtimeToml; @@ -389,7 +390,13 @@ web_search = true ) .expect("TOML deserialization should succeed"); - assert_eq!(cfg.tools, Some(ToolsToml { web_search: None })); + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: None, + }) + ); } #[test] @@ -402,7 +409,72 @@ web_search = false ) .expect("TOML deserialization should succeed"); - assert_eq!(cfg.tools, Some(ToolsToml { web_search: None })); + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: None, + }) + ); +} + +#[test] +fn tools_experimental_request_user_input_defaults_to_enabled() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools.experimental_request_user_input] +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: true }), + }) + ); +} + +#[test] +fn tools_experimental_request_user_input_can_be_disabled() { + let cfg: ConfigToml = toml::from_str( + r#" +[tools.experimental_request_user_input] +enabled = false +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.tools, + Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { enabled: false }), + }) + ); +} + +#[tokio::test] +async fn load_config_resolves_experimental_request_user_input_enabled() -> std::io::Result<()> { + let codex_home = tempdir()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + tools: Some(ToolsToml { + web_search: None, + experimental_request_user_input: Some(ExperimentalRequestUserInput { + enabled: false, + }), + }), + ..ConfigToml::default() + }, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(!config.experimental_request_user_input_enabled); + Ok(()) } #[test] @@ -1554,7 +1626,6 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: .await?; let cwd_root = cwd.path().abs(); - let memories_root = codex_home.path().join("memories").abs(); assert_eq!( config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(vec![ @@ -1576,18 +1647,12 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: }, access: FileSystemAccessMode::Read, }, - FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: memories_root.clone(), - }, - access: FileSystemAccessMode::Write, - }, ]), ); assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![memories_root], + writable_roots: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1816,7 +1881,7 @@ async fn managed_unrestricted_permission_profile_still_enables_network_requireme } #[tokio::test] -async fn permission_profile_override_applies_runtime_roots_to_legacy_projection() +async fn permission_profile_override_keeps_memories_root_out_of_legacy_projection() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; @@ -1851,7 +1916,7 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection( let memories_root = codex_home.path().join("memories").abs(); assert!( - config + !config .permissions .file_system_sandbox_policy() .can_write_path_with_cwd(memories_root.as_path(), cwd.path()) @@ -1859,7 +1924,7 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection( assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![memories_root], + writable_roots: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2757,9 +2822,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() description: Some("Workspace access.".to_string()), }] ); - let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize( - codex_home.path().join("memories"), - )?)?; assert!( config .permissions @@ -2769,7 +2831,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() assert_eq!( &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![external_write_path, memories_root], + writable_roots: vec![external_write_path], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -4514,13 +4576,15 @@ async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Re } #[tokio::test] -async fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> { +async fn workspace_write_includes_configured_writable_root_once_without_memories_root() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let memories_root = codex_home.path().join("memories"); + let writable_root = codex_home.path().join("writable").abs(); let config = Config::load_from_base_config_with_overrides( ConfigToml { sandbox_workspace_write: Some(SandboxWorkspaceWrite { - writable_roots: vec![memories_root.abs()], + writable_roots: vec![writable_root.clone(), writable_root.clone()], ..Default::default() }), ..Default::default() @@ -4540,21 +4604,22 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result } } else { assert!( - memories_root.is_dir(), - "expected memories root directory to exist at {}", + !memories_root.exists(), + "expected config load not to create memories root at {}", memories_root.display() ); let expected_memories_root = memories_root.abs(); match &config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + assert!(!writable_roots.contains(&expected_memories_root)); assert_eq!( writable_roots .iter() - .filter(|root| **root == expected_memories_root) + .filter(|root| **root == writable_root) .count(), 1, "expected single writable root entry for {}", - expected_memories_root.display() + writable_root.display() ); } other => panic!("expected workspace-write policy, got {other:?}"), @@ -4564,6 +4629,62 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result Ok(()) } +#[tokio::test] +async fn memory_tool_makes_memories_root_readable_without_creating_or_widening_writes() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let memories_root = codex_home.path().join("memories"); + let memories_root_abs = memories_root.abs(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + features: Some(FeaturesToml::from(BTreeMap::from([( + "memories".to_string(), + true, + )]))), + sandbox_workspace_write: Some(SandboxWorkspaceWrite { + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + ..Default::default() + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + sandbox_mode: Some(SandboxMode::WorkspaceWrite), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert!( + !memories_root.exists(), + "expected config load not to create memories root at {}", + memories_root.display() + ); + let file_system_policy = config.permissions.file_system_sandbox_policy(); + assert!(file_system_policy.can_read_path_with_cwd(memories_root_abs.as_path(), cwd.path())); + assert!(!file_system_policy.can_write_path_with_cwd(memories_root_abs.as_path(), cwd.path())); + + if cfg!(target_os = "windows") { + match &config.legacy_sandbox_policy() { + SandboxPolicy::ReadOnly { .. } => {} + other => panic!("expected read-only policy on Windows, got {other:?}"), + } + } else { + match &config.legacy_sandbox_policy() { + SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { + assert!(!writable_roots.contains(&memories_root_abs)); + } + other => panic!("expected workspace-write policy, got {other:?}"), + } + } + + Ok(()) +} + #[tokio::test] async fn config_defaults_to_file_cli_auth_store_mode() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -8146,6 +8267,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -8869,6 +8991,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allow_managed_hooks_only: None, allow_appshots: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -8896,6 +9019,47 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s Ok(()) } +#[tokio::test] +async fn windows_sandbox_mode_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"[windows] +sandbox = "unelevated" +"#, + )?; + + let requirements = codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + codex_config::types::WindowsSandboxModeToml::Elevated, + ]), + }), + ..Default::default() + }; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async move { + Ok(Some(requirements)) + })) + .build() + .await?; + + assert_eq!( + config.permissions.windows_sandbox_mode, + Some(codex_config::types::WindowsSandboxModeToml::Elevated) + ); + assert!( + config.startup_warnings.iter().any(|warning| warning + .contains("Configured value for `windows.sandbox` is disallowed by requirements")), + "{:?}", + config.startup_warnings + ); + Ok(()) +} + #[tokio::test] async fn danger_full_access_with_never_is_rejected_when_requirements_force_read_only() -> std::io::Result<()> { @@ -9682,7 +9846,55 @@ enabled = true assert_eq!(config.multi_agent_v2.max_wait_timeout_ms, 3_600_000); assert_eq!(config.multi_agent_v2.default_wait_timeout_ms, 30_000); assert_eq!(config.agent_max_threads, Some(3)); - assert!(!config.multi_agent_v2.non_code_mode_only); + assert_eq!( + config.multi_agent_v2.root_agent_usage_hint_text.as_deref(), + Some(DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT) + ); + assert!( + !config + .multi_agent_v2 + .root_agent_usage_hint_text + .as_deref() + .unwrap_or_default() + .contains("maximum concurrency"), + ); + assert_eq!( + config.multi_agent_v2.subagent_usage_hint_text.as_deref(), + Some(DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT) + ); + assert!( + !config + .multi_agent_v2 + .subagent_usage_hint_text + .as_deref() + .unwrap_or_default() + .contains("maximum concurrency"), + ); + assert!(config.multi_agent_v2.non_code_mode_only); + + Ok(()) +} + +#[tokio::test] +async fn multi_agent_v2_empty_usage_hint_overrides_clear_default_hints() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"[features.multi_agent_v2] +enabled = true +root_agent_usage_hint_text = "" +subagent_usage_hint_text = "" +"#, + )?; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .build() + .await?; + + assert_eq!(config.multi_agent_v2.root_agent_usage_hint_text, None); + assert_eq!(config.multi_agent_v2.subagent_usage_hint_text, None); Ok(()) } diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index ca54a7c3f5..862660ba2e 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -24,6 +24,8 @@ use toml_edit::value; const NOTICE_TABLE_KEY: &str = "notice"; +mod document_helpers; + /// Discrete config mutations supported by the persistence engine. #[derive(Clone, Debug)] pub enum ConfigEdit { @@ -179,7 +181,7 @@ pub fn keymap_binding_clear_edit(context: &str, action: &str) -> ConfigEdit { pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> Vec { let mut shown_count_entries: Vec<_> = shown_count.iter().collect(); - shown_count_entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right)); + shown_count_entries.sort_unstable_by_key(|(left, _)| *left); let mut edits = vec![ConfigEdit::ClearPath { segments: vec!["tui".to_string(), "model_availability_nux".to_string()], @@ -198,319 +200,6 @@ pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> edits } -// TODO(jif) move to a dedicated file -mod document_helpers { - use codex_config::types::AppToolApproval; - use codex_config::types::McpServerConfig; - use codex_config::types::McpServerEnvVar; - use codex_config::types::McpServerToolConfig; - use codex_config::types::McpServerTransportConfig; - use codex_config::types::ToolSuggestDisabledTool; - use codex_config::types::ToolSuggestDiscoverableType; - use toml_edit::Array as TomlArray; - use toml_edit::InlineTable; - use toml_edit::Item as TomlItem; - use toml_edit::Table as TomlTable; - use toml_edit::Value as TomlValue; - use toml_edit::value; - - pub(super) fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> { - match item { - TomlItem::Table(table) => Some(table), - TomlItem::Value(value) => { - if let Some(inline) = value.as_inline_table() { - *item = TomlItem::Table(table_from_inline(inline)); - item.as_table_mut() - } else { - *item = TomlItem::Table(new_implicit_table()); - item.as_table_mut() - } - } - TomlItem::None => { - *item = TomlItem::Table(new_implicit_table()); - item.as_table_mut() - } - _ => None, - } - } - - pub(super) fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTable> { - match item { - TomlItem::Table(table) => Some(table), - TomlItem::Value(value) => { - let inline = value.as_inline_table()?; - *item = TomlItem::Table(table_from_inline(inline)); - item.as_table_mut() - } - _ => None, - } - } - - fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable { - let mut entry = TomlTable::new(); - entry.set_implicit(false); - - match &config.transport { - McpServerTransportConfig::Stdio { - command, - args, - env, - env_vars, - cwd, - } => { - entry["command"] = value(command.clone()); - if !args.is_empty() { - entry["args"] = array_from_iter(args.iter().cloned()); - } - if let Some(env) = env - && !env.is_empty() - { - entry["env"] = table_from_pairs(env.iter()); - } - if !env_vars.is_empty() { - entry["env_vars"] = array_from_env_vars(env_vars); - } - if let Some(cwd) = cwd { - entry["cwd"] = value(cwd.to_string_lossy().to_string()); - } - } - McpServerTransportConfig::StreamableHttp { - url, - bearer_token_env_var, - http_headers, - env_http_headers, - } => { - entry["url"] = value(url.clone()); - if let Some(env_var) = bearer_token_env_var { - entry["bearer_token_env_var"] = value(env_var.clone()); - } - if let Some(headers) = http_headers - && !headers.is_empty() - { - entry["http_headers"] = table_from_pairs(headers.iter()); - } - if let Some(headers) = env_http_headers - && !headers.is_empty() - { - entry["env_http_headers"] = table_from_pairs(headers.iter()); - } - } - } - - if !config.enabled { - entry["enabled"] = value(false); - } - if !config.is_local_environment() { - entry["environment_id"] = value(config.environment_id.clone()); - } - if config.required { - entry["required"] = value(true); - } - if config.supports_parallel_tool_calls { - entry["supports_parallel_tool_calls"] = value(true); - } - if let Some(timeout) = config.startup_timeout_sec { - entry["startup_timeout_sec"] = value(timeout.as_secs_f64()); - } - if let Some(timeout) = config.tool_timeout_sec { - entry["tool_timeout_sec"] = value(timeout.as_secs_f64()); - } - if let Some(approval_mode) = config.default_tools_approval_mode { - entry["default_tools_approval_mode"] = value(match approval_mode { - AppToolApproval::Auto => "auto", - AppToolApproval::Prompt => "prompt", - AppToolApproval::Approve => "approve", - }); - } - if let Some(enabled_tools) = &config.enabled_tools - && !enabled_tools.is_empty() - { - entry["enabled_tools"] = array_from_iter(enabled_tools.iter().cloned()); - } - if let Some(disabled_tools) = &config.disabled_tools - && !disabled_tools.is_empty() - { - entry["disabled_tools"] = array_from_iter(disabled_tools.iter().cloned()); - } - if let Some(scopes) = &config.scopes - && !scopes.is_empty() - { - entry["scopes"] = array_from_iter(scopes.iter().cloned()); - } - if let Some(oauth) = &config.oauth - && let Some(client_id) = &oauth.client_id - && !client_id.is_empty() - { - let mut oauth_table = TomlTable::new(); - oauth_table.set_implicit(false); - oauth_table["client_id"] = value(client_id.clone()); - entry["oauth"] = TomlItem::Table(oauth_table); - } - if let Some(resource) = &config.oauth_resource - && !resource.is_empty() - { - entry["oauth_resource"] = value(resource.clone()); - } - if !config.tools.is_empty() { - let mut tools = new_implicit_table(); - let mut tool_entries: Vec<_> = config.tools.iter().collect(); - tool_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - for (name, tool_config) in tool_entries { - tools.insert(name, serialize_mcp_server_tool(tool_config)); - } - entry.insert("tools", TomlItem::Table(tools)); - } - - entry - } - - fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem { - let mut entry = TomlTable::new(); - entry.set_implicit(false); - if let Some(approval_mode) = config.approval_mode { - entry["approval_mode"] = value(match approval_mode { - AppToolApproval::Auto => "auto", - AppToolApproval::Prompt => "prompt", - AppToolApproval::Approve => "approve", - }); - } - TomlItem::Table(entry) - } - - pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { - TomlItem::Table(serialize_mcp_server_table(config)) - } - - pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> InlineTable { - serialize_mcp_server_table(config).into_inline_table() - } - - pub(super) fn merge_inline_table(existing: &mut InlineTable, replacement: InlineTable) { - existing.retain(|key, _| replacement.get(key).is_some()); - - for (key, value) in replacement.iter() { - if let Some(existing_value) = existing.get_mut(key) { - let mut updated_value = value.clone(); - *updated_value.decor_mut() = existing_value.decor().clone(); - *existing_value = updated_value; - } else { - existing.insert(key.to_string(), value.clone()); - } - } - } - - fn table_from_inline(inline: &InlineTable) -> TomlTable { - let mut table = new_implicit_table(); - for (key, value) in inline.iter() { - let mut value = value.clone(); - let decor = value.decor_mut(); - decor.set_suffix(""); - table.insert(key, TomlItem::Value(value)); - } - table - } - - pub(super) fn new_implicit_table() -> TomlTable { - let mut table = TomlTable::new(); - table.set_implicit(true); - table - } - - pub(super) fn parse_tool_suggest_disabled_tool( - value: &TomlValue, - ) -> Option { - let table = value.as_inline_table()?; - let kind = match table.get("type").and_then(TomlValue::as_str) { - Some("connector") => ToolSuggestDiscoverableType::Connector, - Some("plugin") => ToolSuggestDiscoverableType::Plugin, - _ => return None, - }; - let id = table.get("id").and_then(TomlValue::as_str)?; - Some(ToolSuggestDisabledTool { - kind, - id: id.to_string(), - }) - } - - pub(super) fn parse_tool_suggest_disabled_tool_table( - table: &TomlTable, - ) -> Option { - let kind = match table.get("type").and_then(TomlItem::as_str) { - Some("connector") => ToolSuggestDiscoverableType::Connector, - Some("plugin") => ToolSuggestDiscoverableType::Plugin, - _ => return None, - }; - let id = table.get("id").and_then(TomlItem::as_str)?; - Some(ToolSuggestDisabledTool { - kind, - id: id.to_string(), - }) - } - - pub(super) fn tool_suggest_disabled_tools_value( - disabled_tools: &[ToolSuggestDisabledTool], - ) -> TomlItem { - let mut array = TomlArray::new(); - for disabled_tool in disabled_tools { - let mut table = InlineTable::new(); - table.insert( - "type", - match disabled_tool.kind { - ToolSuggestDiscoverableType::Connector => "connector", - ToolSuggestDiscoverableType::Plugin => "plugin", - } - .into(), - ); - table.insert("id", disabled_tool.id.clone().into()); - array.push(table); - } - TomlItem::Value(array.into()) - } - - fn array_from_iter(iter: I) -> TomlItem - where - I: Iterator, - { - let mut array = TomlArray::new(); - for value in iter { - array.push(value); - } - TomlItem::Value(array.into()) - } - - fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem { - let mut array = TomlArray::new(); - for env_var in env_vars { - match env_var { - McpServerEnvVar::Name(name) => array.push(name.clone()), - McpServerEnvVar::Config { name, source } => { - let mut table = InlineTable::new(); - table.insert("name", name.clone().into()); - if let Some(source) = source { - table.insert("source", source.clone().into()); - } - array.push(table); - } - } - } - TomlItem::Value(array.into()) - } - - fn table_from_pairs<'a, I>(pairs: I) -> TomlItem - where - I: IntoIterator, - { - let mut entries: Vec<_> = pairs.into_iter().collect(); - entries.sort_by(|(a, _), (b, _)| a.cmp(b)); - let mut table = TomlTable::new(); - table.set_implicit(false); - for (key, val) in entries { - table.insert(key, value(val.clone())); - } - TomlItem::Table(table) - } -} - struct ConfigDocument { doc: DocumentMut, } diff --git a/codex-rs/core/src/config/edit/document_helpers.rs b/codex-rs/core/src/config/edit/document_helpers.rs new file mode 100644 index 0000000000..5b99306eb9 --- /dev/null +++ b/codex-rs/core/src/config/edit/document_helpers.rs @@ -0,0 +1,309 @@ +use codex_config::types::AppToolApproval; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerEnvVar; +use codex_config::types::McpServerToolConfig; +use codex_config::types::McpServerTransportConfig; +use codex_config::types::ToolSuggestDisabledTool; +use codex_config::types::ToolSuggestDiscoverableType; +use toml_edit::Array as TomlArray; +use toml_edit::InlineTable; +use toml_edit::Item as TomlItem; +use toml_edit::Table as TomlTable; +use toml_edit::Value as TomlValue; +use toml_edit::value; + +pub(super) fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> { + match item { + TomlItem::Table(table) => Some(table), + TomlItem::Value(value) => { + if let Some(inline) = value.as_inline_table() { + *item = TomlItem::Table(table_from_inline(inline)); + item.as_table_mut() + } else { + *item = TomlItem::Table(new_implicit_table()); + item.as_table_mut() + } + } + TomlItem::None => { + *item = TomlItem::Table(new_implicit_table()); + item.as_table_mut() + } + _ => None, + } +} + +pub(super) fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTable> { + match item { + TomlItem::Table(table) => Some(table), + TomlItem::Value(value) => { + let inline = value.as_inline_table()?; + *item = TomlItem::Table(table_from_inline(inline)); + item.as_table_mut() + } + _ => None, + } +} + +fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable { + let mut entry = TomlTable::new(); + entry.set_implicit(false); + + match &config.transport { + McpServerTransportConfig::Stdio { + command, + args, + env, + env_vars, + cwd, + } => { + entry["command"] = value(command.clone()); + if !args.is_empty() { + entry["args"] = array_from_iter(args.iter().cloned()); + } + if let Some(env) = env + && !env.is_empty() + { + entry["env"] = table_from_pairs(env.iter()); + } + if !env_vars.is_empty() { + entry["env_vars"] = array_from_env_vars(env_vars); + } + if let Some(cwd) = cwd { + entry["cwd"] = value(cwd.to_string_lossy().to_string()); + } + } + McpServerTransportConfig::StreamableHttp { + url, + bearer_token_env_var, + http_headers, + env_http_headers, + } => { + entry["url"] = value(url.clone()); + if let Some(env_var) = bearer_token_env_var { + entry["bearer_token_env_var"] = value(env_var.clone()); + } + if let Some(headers) = http_headers + && !headers.is_empty() + { + entry["http_headers"] = table_from_pairs(headers.iter()); + } + if let Some(headers) = env_http_headers + && !headers.is_empty() + { + entry["env_http_headers"] = table_from_pairs(headers.iter()); + } + } + } + + if !config.enabled { + entry["enabled"] = value(false); + } + if !config.is_local_environment() { + entry["environment_id"] = value(config.environment_id.clone()); + } + if config.required { + entry["required"] = value(true); + } + if config.supports_parallel_tool_calls { + entry["supports_parallel_tool_calls"] = value(true); + } + if let Some(timeout) = config.startup_timeout_sec { + entry["startup_timeout_sec"] = value(timeout.as_secs_f64()); + } + if let Some(timeout) = config.tool_timeout_sec { + entry["tool_timeout_sec"] = value(timeout.as_secs_f64()); + } + if let Some(approval_mode) = config.default_tools_approval_mode { + entry["default_tools_approval_mode"] = value(match approval_mode { + AppToolApproval::Auto => "auto", + AppToolApproval::Prompt => "prompt", + AppToolApproval::Approve => "approve", + }); + } + if let Some(enabled_tools) = &config.enabled_tools + && !enabled_tools.is_empty() + { + entry["enabled_tools"] = array_from_iter(enabled_tools.iter().cloned()); + } + if let Some(disabled_tools) = &config.disabled_tools + && !disabled_tools.is_empty() + { + entry["disabled_tools"] = array_from_iter(disabled_tools.iter().cloned()); + } + if let Some(scopes) = &config.scopes + && !scopes.is_empty() + { + entry["scopes"] = array_from_iter(scopes.iter().cloned()); + } + if let Some(oauth) = &config.oauth + && let Some(client_id) = &oauth.client_id + && !client_id.is_empty() + { + let mut oauth_table = TomlTable::new(); + oauth_table.set_implicit(false); + oauth_table["client_id"] = value(client_id.clone()); + entry["oauth"] = TomlItem::Table(oauth_table); + } + if let Some(resource) = &config.oauth_resource + && !resource.is_empty() + { + entry["oauth_resource"] = value(resource.clone()); + } + if !config.tools.is_empty() { + let mut tools = new_implicit_table(); + let mut tool_entries: Vec<_> = config.tools.iter().collect(); + tool_entries.sort_by_key(|(name, _)| *name); + for (name, tool_config) in tool_entries { + tools.insert(name, serialize_mcp_server_tool(tool_config)); + } + entry.insert("tools", TomlItem::Table(tools)); + } + + entry +} + +fn serialize_mcp_server_tool(config: &McpServerToolConfig) -> TomlItem { + let mut entry = TomlTable::new(); + entry.set_implicit(false); + if let Some(approval_mode) = config.approval_mode { + entry["approval_mode"] = value(match approval_mode { + AppToolApproval::Auto => "auto", + AppToolApproval::Prompt => "prompt", + AppToolApproval::Approve => "approve", + }); + } + TomlItem::Table(entry) +} + +pub(super) fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { + TomlItem::Table(serialize_mcp_server_table(config)) +} + +pub(super) fn serialize_mcp_server_inline(config: &McpServerConfig) -> InlineTable { + serialize_mcp_server_table(config).into_inline_table() +} + +pub(super) fn merge_inline_table(existing: &mut InlineTable, replacement: InlineTable) { + existing.retain(|key, _| replacement.get(key).is_some()); + + for (key, value) in replacement.iter() { + if let Some(existing_value) = existing.get_mut(key) { + let mut updated_value = value.clone(); + *updated_value.decor_mut() = existing_value.decor().clone(); + *existing_value = updated_value; + } else { + existing.insert(key.to_string(), value.clone()); + } + } +} + +fn table_from_inline(inline: &InlineTable) -> TomlTable { + let mut table = new_implicit_table(); + for (key, value) in inline.iter() { + let mut value = value.clone(); + let decor = value.decor_mut(); + decor.set_suffix(""); + table.insert(key, TomlItem::Value(value)); + } + table +} + +pub(super) fn new_implicit_table() -> TomlTable { + let mut table = TomlTable::new(); + table.set_implicit(true); + table +} + +pub(super) fn parse_tool_suggest_disabled_tool( + value: &TomlValue, +) -> Option { + let table = value.as_inline_table()?; + let kind = match table.get("type").and_then(TomlValue::as_str) { + Some("connector") => ToolSuggestDiscoverableType::Connector, + Some("plugin") => ToolSuggestDiscoverableType::Plugin, + _ => return None, + }; + let id = table.get("id").and_then(TomlValue::as_str)?; + Some(ToolSuggestDisabledTool { + kind, + id: id.to_string(), + }) +} + +pub(super) fn parse_tool_suggest_disabled_tool_table( + table: &TomlTable, +) -> Option { + let kind = match table.get("type").and_then(TomlItem::as_str) { + Some("connector") => ToolSuggestDiscoverableType::Connector, + Some("plugin") => ToolSuggestDiscoverableType::Plugin, + _ => return None, + }; + let id = table.get("id").and_then(TomlItem::as_str)?; + Some(ToolSuggestDisabledTool { + kind, + id: id.to_string(), + }) +} + +pub(super) fn tool_suggest_disabled_tools_value( + disabled_tools: &[ToolSuggestDisabledTool], +) -> TomlItem { + let mut array = TomlArray::new(); + for disabled_tool in disabled_tools { + let mut table = InlineTable::new(); + table.insert( + "type", + match disabled_tool.kind { + ToolSuggestDiscoverableType::Connector => "connector", + ToolSuggestDiscoverableType::Plugin => "plugin", + } + .into(), + ); + table.insert("id", disabled_tool.id.clone().into()); + array.push(table); + } + TomlItem::Value(array.into()) +} + +fn array_from_iter(iter: I) -> TomlItem +where + I: Iterator, +{ + let mut array = TomlArray::new(); + for value in iter { + array.push(value); + } + TomlItem::Value(array.into()) +} + +fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem { + let mut array = TomlArray::new(); + for env_var in env_vars { + match env_var { + McpServerEnvVar::Name(name) => array.push(name.clone()), + McpServerEnvVar::Config { name, source } => { + let mut table = InlineTable::new(); + table.insert("name", name.clone().into()); + if let Some(source) = source { + table.insert("source", source.clone().into()); + } + array.push(table); + } + } + } + TomlItem::Value(array.into()) +} + +fn table_from_pairs<'a, I>(pairs: I) -> TomlItem +where + I: IntoIterator, +{ + let mut entries: Vec<_> = pairs.into_iter().collect(); + entries.sort_by_key(|(key, _)| *key); + let mut table = TomlTable::new(); + table.set_implicit(false); + for (key, val) in entries { + table.insert(key, value(val.clone())); + } + TomlItem::Table(table) +} diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 3115921c2d..ff666b51e9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -189,6 +189,44 @@ pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usiz pub(crate) const DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS: i64 = 10_000; pub(crate) const DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS: i64 = 3600 * 1000; pub(crate) const DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000; +const DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT: &str = r#"You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals. + +At the start of your turn, you are the active agent. +You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. +All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + +You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent without triggering a turn. +Child agents can also spawn their own sub-agents. +You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter. + +You will receive messages in the analysis channel in the form: +``` +Message Type: MESSAGE | FINAL_ANSWER +Sender: +Payload: + +``` +They may be addressed as to=/root +"#; +const DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT: &str = r#"You are an agent in a team of agents collaborating to complete a task. + +You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents. All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools. + +You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task and trigger a turn, and `send_message` to pass a message to a running agent. +Child agents can also spawn their own sub-agents. + +When you provide a response in the final channel, that content is immediately delivered back to your parent agent. + +You will receive messages in the analysis channel in the form: +``` +Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER +Task name: # only for NEW_TASK -- this determines your identity +Sender: +Payload: + +``` +You may also see them addressed as to=/root/..., which indicates your identity is /root/... +"#; pub(crate) const HARD_MIN_MULTI_AGENT_V2_TIMEOUT_MS: i64 = 0; pub(crate) const HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS: i64 = DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS; @@ -939,6 +977,9 @@ pub struct Config { /// Additional parameters for the web search tool when it is enabled. pub web_search_config: Option, + /// Whether to register the experimental request_user_input tool. + pub experimental_request_user_input_enabled: bool, + /// If set to `true`, used only the experimental unified exec tool. pub use_experimental_unified_exec_tool: bool, @@ -1016,11 +1057,15 @@ impl Default for MultiAgentV2Config { default_wait_timeout_ms: DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS, usage_hint_enabled: true, usage_hint_text: None, - root_agent_usage_hint_text: None, - subagent_usage_hint_text: None, + root_agent_usage_hint_text: Some( + DEFAULT_MULTI_AGENT_V2_ROOT_AGENT_USAGE_HINT_TEXT.to_string(), + ), + subagent_usage_hint_text: Some( + DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT.to_string(), + ), tool_namespace: None, hide_spawn_agent_metadata: false, - non_code_mode_only: false, + non_code_mode_only: true, } } } @@ -2205,6 +2250,14 @@ fn resolve_web_search_config(config_toml: &ConfigToml) -> Option bool { + config_toml + .tools + .as_ref() + .and_then(|tools| tools.experimental_request_user_input.as_ref()) + .is_none_or(|config| config.enabled) +} + fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config { let base = multi_agent_v2_toml_config(config_toml.features.as_ref()); let default = MultiAgentV2Config::default(); @@ -2228,14 +2281,14 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config .and_then(|config| config.usage_hint_text.as_ref()) .cloned() .or(default.usage_hint_text); - let root_agent_usage_hint_text = base - .and_then(|config| config.root_agent_usage_hint_text.as_ref()) - .cloned() - .or(default.root_agent_usage_hint_text); - let subagent_usage_hint_text = base - .and_then(|config| config.subagent_usage_hint_text.as_ref()) - .cloned() - .or(default.subagent_usage_hint_text); + let root_agent_usage_hint_text = resolve_optional_prompt_text( + base.map(|config| &config.root_agent_usage_hint_text), + default.root_agent_usage_hint_text, + ); + let subagent_usage_hint_text = resolve_optional_prompt_text( + base.map(|config| &config.subagent_usage_hint_text), + default.subagent_usage_hint_text, + ); let tool_namespace = base .and_then(|config| config.tool_namespace.as_ref()) .cloned() @@ -2276,6 +2329,17 @@ fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalRe } } +fn resolve_optional_prompt_text( + configured: Option<&Option>, + default: Option, +) -> Option { + match configured { + Some(Some(value)) if value.is_empty() => None, + Some(Some(value)) => Some(value.clone()), + Some(None) | None => default, + } +} + fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> { match features?.multi_agent_v2.as_ref()? { FeatureToml::Enabled(_) => None, @@ -2457,6 +2521,7 @@ impl Config { approval_policy: mut constrained_approval_policy, approvals_reviewer: mut constrained_approvals_reviewer, permission_profile: mut constrained_permission_profile, + windows_sandbox_mode: mut constrained_windows_sandbox_mode, web_search_mode: mut constrained_web_search_mode, allow_managed_hooks_only: _, allow_appshots: _, @@ -2568,7 +2633,28 @@ impl Config { &mut startup_warnings, )?; let enable_network_proxy = features.enabled(Feature::NetworkProxy); - let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg); + let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg); + // Keep the configured mode separate so a requirement-constrained mode + // does not look like it was explicitly selected in config. + let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| { + match WindowsSandboxLevel::from_features(&features) { + WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated), + WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated), + WindowsSandboxLevel::Disabled => None, + } + }); + apply_requirement_constrained_value( + "windows.sandbox", + selected_windows_sandbox_mode, + &mut constrained_windows_sandbox_mode, + &mut startup_warnings, + )?; + let effective_windows_sandbox_mode = *constrained_windows_sandbox_mode.get(); + let windows_sandbox_mode = if constrained_windows_sandbox_mode.source.is_some() { + effective_windows_sandbox_mode + } else { + configured_windows_sandbox_mode + }; let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg); let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({ use std::env; @@ -2626,14 +2712,13 @@ impl Config { )); } - let windows_sandbox_level = match windows_sandbox_mode { + let windows_sandbox_level = match effective_windows_sandbox_mode { Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated, Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken, - None => WindowsSandboxLevel::from_features(&features), + None => WindowsSandboxLevel::Disabled, }; + let memories_config: MemoriesConfig = cfg.memories.clone().unwrap_or_default().into(); let memories_root = memory_root(&codex_home); - std::fs::create_dir_all(&memories_root)?; - let internal_writable_roots = vec![memories_root]; let profiles_are_active = effective_permission_selection.profiles_are_active( default_permissions_override.as_deref(), @@ -2701,8 +2786,8 @@ impl Config { file_system_sandbox_policy, mut active_permission_profile, mut profile_workspace_roots, - ) = if let Some(mut permission_profile) = permission_profile { - let (mut file_system_sandbox_policy, network_sandbox_policy) = + ) = if let Some(permission_profile) = permission_profile { + let (file_system_sandbox_policy, _network_sandbox_policy) = permission_profile.to_runtime_permissions(); let configured_network_proxy_config = if profile_allows_configured_network_proxy(&permission_profile) @@ -2726,30 +2811,6 @@ impl Config { } else { NetworkProxyConfig::default() }; - let materialized_file_system_sandbox_policy = file_system_sandbox_policy - .clone() - .materialize_project_roots_with_workspace_roots(&workspace_roots); - let materialized_permission_profile = - PermissionProfile::from_runtime_permissions_with_enforcement( - permission_profile.enforcement(), - &materialized_file_system_sandbox_policy, - network_sandbox_policy, - ); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &materialized_permission_profile, - &materialized_file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); - if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { - file_system_sandbox_policy = file_system_sandbox_policy - .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); - permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - permission_profile.enforcement(), - &file_system_sandbox_policy, - network_sandbox_policy, - ); - } ( configured_network_proxy_config, permission_profile, @@ -2794,7 +2855,7 @@ impl Config { dedupe_absolute_paths(&mut configured_workspace_roots); file_system_sandbox_policy = file_system_sandbox_policy .with_materialized_project_roots_for_workspace_roots(&configured_workspace_roots); - let mut permission_profile = if let Some(permission_profile) = + let permission_profile = if let Some(permission_profile) = builtin_permission_profile(default_permissions, builtin_workspace_write_settings) { permission_profile @@ -2804,30 +2865,6 @@ impl Config { network_sandbox_policy, ) }; - let materialized_file_system_sandbox_policy = file_system_sandbox_policy - .clone() - .materialize_project_roots_with_workspace_roots(&workspace_roots); - let materialized_permission_profile = - PermissionProfile::from_runtime_permissions_with_enforcement( - permission_profile.enforcement(), - &materialized_file_system_sandbox_policy, - network_sandbox_policy, - ); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &materialized_permission_profile, - &materialized_file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); - if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { - file_system_sandbox_policy = file_system_sandbox_policy - .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); - permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - permission_profile.enforcement(), - &file_system_sandbox_policy, - network_sandbox_policy, - ); - } let active_permission_profile = if using_implicit_builtin_profile && default_permissions == BUILT_IN_WORKSPACE_PROFILE && cfg.sandbox_workspace_write.is_some() @@ -2885,29 +2922,8 @@ impl Config { ); permission_profile = PermissionProfile::read_only(); } - let (mut file_system_sandbox_policy, network_sandbox_policy) = + let (file_system_sandbox_policy, _network_sandbox_policy) = permission_profile.to_runtime_permissions(); - let materialized_file_system_sandbox_policy = permission_profile - .clone() - .materialize_project_roots_with_workspace_roots(&workspace_roots) - .file_system_sandbox_policy(); - if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed) - && materialized_file_system_sandbox_policy.can_write_path_with_cwd( - resolved_cwd.as_path(), - resolved_cwd.as_path(), - ) - && !materialized_file_system_sandbox_policy.has_full_disk_write_access() - { - // Keep Codex runtime write access while storing the runtime - // workspace roots separately on the thread. - file_system_sandbox_policy = file_system_sandbox_policy - .with_additional_legacy_workspace_writable_roots(&internal_writable_roots); - permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - permission_profile.enforcement(), - &file_system_sandbox_policy, - network_sandbox_policy, - ); - } ( configured_network_proxy_config, permission_profile, @@ -2964,6 +2980,8 @@ impl Config { let web_search_mode = resolve_web_search_mode(&cfg, &features).unwrap_or(WebSearchMode::Cached); let web_search_config = resolve_web_search_config(&cfg); + let experimental_request_user_input_enabled = + resolve_experimental_request_user_input_enabled(&cfg); let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg); let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) { let base = apps_mcp_path_override_toml_config(cfg.features.as_ref()); @@ -3324,11 +3342,14 @@ impl Config { network_requirements, &network_permission_profile, )?; - let helper_readable_roots = get_readable_roots_required_for_codex_runtime( + let mut helper_readable_roots = get_readable_roots_required_for_codex_runtime( &codex_home, zsh_path.as_ref(), main_execve_wrapper_exe.as_ref(), ); + if features.enabled(Feature::MemoryTool) && memories_config.use_memories { + helper_readable_roots.push(memories_root); + } let effective_permission_profile = constrained_permission_profile.value.get().clone(); let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) = effective_permission_profile.to_runtime_permissions(); @@ -3438,7 +3459,7 @@ impl Config { agent_max_threads, agent_max_depth, agent_roles, - memories: cfg.memories.unwrap_or_default().into(), + memories: memories_config, agent_job_max_runtime_seconds, agent_interrupt_message_enabled, codex_home, @@ -3517,6 +3538,7 @@ impl Config { forced_login_method, web_search_mode: constrained_web_search_mode.value, web_search_config, + experimental_request_user_input_enabled, use_experimental_unified_exec_tool, background_terminal_max_timeout, ghost_snapshot, diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 3aaa7d66f8..a8546858f1 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -176,8 +176,8 @@ pub(crate) fn apply_network_proxy_feature_config( NetworkProxyUnixSocketPermissionToml::Allow => { NetworkUnixSocketPermissionToml::Allow } - NetworkProxyUnixSocketPermissionToml::None => { - NetworkUnixSocketPermissionToml::None + NetworkProxyUnixSocketPermissionToml::Deny => { + NetworkUnixSocketPermissionToml::Deny } }; (path.clone(), permission) diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 8f9d6732e4..41f4179ba8 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -149,7 +149,7 @@ fn network_permission_containers_project_allowed_and_denied_entries() { ), ( "/tmp/ignored.sock".to_string(), - NetworkUnixSocketPermissionToml::None, + NetworkUnixSocketPermissionToml::Deny, ), ]), }; @@ -211,7 +211,7 @@ fn network_toml_overlays_unix_socket_permissions_by_path() { ), ( "/tmp/override.sock".to_string(), - NetworkUnixSocketPermissionToml::None, + NetworkUnixSocketPermissionToml::Deny, ), ]), }), @@ -233,7 +233,7 @@ fn network_toml_overlays_unix_socket_permissions_by_path() { ), ( "/tmp/override.sock".to_string(), - ProxyNetworkUnixSocketPermission::None, + ProxyNetworkUnixSocketPermission::Deny, ), ]), }) @@ -265,6 +265,7 @@ enabled = true [base.network.unix_sockets] "/tmp/base.sock" = "allow" +"/tmp/blocked.sock" = "deny" [child] extends = "base" @@ -319,6 +320,7 @@ allow_local_binding = true [network.unix_sockets] "/tmp/base.sock" = "allow" +"/tmp/blocked.sock" = "deny" "/tmp/child.sock" = "allow" "#, ) diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 09717bb121..55a941e8c4 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -113,8 +113,9 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( config: &Config, auth: Option<&CodexAuth>, accessible_connectors: &[AppInfo], + loaded_plugin_app_connector_ids: &[String], ) -> anyhow::Result> { - let connector_ids = tool_suggest_connector_ids(config).await; + let connector_ids = tool_suggest_connector_ids(config, loaded_plugin_app_connector_ids); let directory_connectors = codex_connectors::merge::merge_plugin_connectors( cached_directory_connectors_for_tool_suggest_with_auth(config, auth).await, connector_ids.iter().cloned(), @@ -128,10 +129,11 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth( ) .into_iter() .map(DiscoverableTool::from); - let discoverable_plugins = list_tool_suggest_discoverable_plugins(config) - .await? - .into_iter() - .map(DiscoverableTool::from); + let discoverable_plugins = + list_tool_suggest_discoverable_plugins(config, loaded_plugin_app_connector_ids) + .await? + .into_iter() + .map(DiscoverableTool::from); Ok(discoverable_connectors .chain(discoverable_plugins) .collect()) @@ -404,15 +406,13 @@ fn write_cached_accessible_connectors( }); } -async fn tool_suggest_connector_ids(config: &Config) -> HashSet { - let plugins_input = config.plugins_config_input(); - let mut connector_ids = PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config(&plugins_input) - .await - .capability_summaries() +fn tool_suggest_connector_ids( + config: &Config, + loaded_plugin_app_connector_ids: &[String], +) -> HashSet { + let mut connector_ids = loaded_plugin_app_connector_ids .iter() - .flat_map(|plugin| plugin.app_connector_ids.iter()) - .map(|connector_id| connector_id.0.clone()) + .cloned() .collect::>(); connector_ids.extend( config diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index cce2491679..285df9a29a 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -31,13 +31,13 @@ use std::sync::Arc; use tempfile::tempdir; fn annotations(destructive_hint: Option, open_world_hint: Option) -> ToolAnnotations { - ToolAnnotations { + ToolAnnotations::from_raw( + /*title*/ None, + /*read_only_hint*/ None, destructive_hint, - idempotent_hint: None, + /*idempotent_hint*/ None, open_world_hint, - read_only_hint: None, - title: None, - } + ) } fn app(id: &str) -> AppInfo { @@ -63,17 +63,7 @@ fn plugin_names(names: &[&str]) -> Vec { } fn test_tool_definition(tool_name: &str) -> Tool { - Tool { - name: tool_name.to_string().into(), - title: None, - description: None, - input_schema: Arc::new(JsonObject::default()), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: None, - } + Tool::new_with_raw(tool_name.to_string(), None, Arc::new(JsonObject::default())) } fn codex_app_tool( @@ -243,17 +233,11 @@ fn accessible_connectors_from_mcp_tools_preserves_description() { callable_name: "calendar_create_event".to_string(), callable_namespace: "mcp__codex_apps__calendar".to_string(), namespace_description: Some("Plan events".to_string()), - tool: Tool { - name: "calendar_create_event".to_string().into(), - title: None, - description: Some("Create a calendar event".into()), - input_schema: Arc::new(JsonObject::default()), - output_schema: None, - annotations: None, - execution: None, - icons: None, - meta: None, - }, + tool: Tool::new( + "calendar_create_event", + "Create a calendar event", + Arc::new(JsonObject::default()), + ), connector_id: Some("calendar".to_string()), connector_name: Some("Calendar".to_string()), plugin_display_names: Vec::new(), @@ -1196,7 +1180,7 @@ discoverables = [ .expect("config should load"); assert_eq!( - tool_suggest_connector_ids(&config).await, + tool_suggest_connector_ids(&config, &[]), HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()]) ); } @@ -1225,7 +1209,7 @@ disabled_tools = [ .expect("config should load"); assert_eq!( - tool_suggest_connector_ids(&config).await, + tool_suggest_connector_ids(&config, &[]), HashSet::from(["connector_gmail".to_string()]) ); } @@ -1254,7 +1238,7 @@ discoverables = [ let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let discoverable_tools = - list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[]) + list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[], &[]) .await .expect("discoverable tools should load"); @@ -1265,3 +1249,39 @@ discoverables = [ ))] ); } + +#[tokio::test] +async fn tool_suggest_includes_connectors_from_loaded_plugin_apps() { + let codex_home = tempdir().expect("tempdir should succeed"); + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[features] +apps = true +"#, + ) + .expect("write config"); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config should load"); + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()]; + + let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth( + &config, + Some(&auth), + &[], + &loaded_plugin_app_connector_ids, + ) + .await + .expect("discoverable tools should load"); + + assert_eq!( + discoverable_tools, + vec![DiscoverableTool::from(plugin_connector_to_app_info( + "asdk_app_databricks_workspace".to_string(), + ))] + ); +} diff --git a/codex-rs/core/src/context/contextual_user_message.rs b/codex-rs/core/src/context/contextual_user_message.rs index c7ada2317f..79768dd3a3 100644 --- a/codex-rs/core/src/context/contextual_user_message.rs +++ b/codex-rs/core/src/context/contextual_user_message.rs @@ -2,10 +2,11 @@ use codex_protocol::items::HookPromptItem; use codex_protocol::items::parse_hook_prompt_fragment; use codex_protocol::models::ContentItem; +use super::AdditionalContextUserFragment; use super::EnvironmentContext; use super::FragmentRegistration; use super::FragmentRegistrationProxy; -use super::GoalContext; +use super::InternalModelContextFragment; use super::LegacyApplyPatchExecCommandWarning; use super::LegacyModelMismatchWarning; use super::LegacyUnifiedExecProcessLimitWarning; @@ -19,6 +20,8 @@ static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); +static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy = + FragmentRegistrationProxy::new(); static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy = @@ -27,8 +30,9 @@ static TURN_ABORTED_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); static SUBAGENT_NOTIFICATION_REGISTRATION: FragmentRegistrationProxy = FragmentRegistrationProxy::new(); -static GOAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy = - FragmentRegistrationProxy::new(); +static INTERNAL_MODEL_CONTEXT_REGISTRATION: FragmentRegistrationProxy< + InternalModelContextFragment, +> = FragmentRegistrationProxy::new(); static LEGACY_UNIFIED_EXEC_PROCESS_LIMIT_WARNING_REGISTRATION: FragmentRegistrationProxy< LegacyUnifiedExecProcessLimitWarning, > = FragmentRegistrationProxy::new(); @@ -42,11 +46,12 @@ static LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION: FragmentRegistrationProxy< static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[ &USER_INSTRUCTIONS_REGISTRATION, &ENVIRONMENT_CONTEXT_REGISTRATION, + &ADDITIONAL_CONTEXT_REGISTRATION, &SKILL_INSTRUCTIONS_REGISTRATION, &USER_SHELL_COMMAND_REGISTRATION, &TURN_ABORTED_REGISTRATION, &SUBAGENT_NOTIFICATION_REGISTRATION, - &GOAL_CONTEXT_REGISTRATION, + &INTERNAL_MODEL_CONTEXT_REGISTRATION, &LEGACY_UNIFIED_EXEC_PROCESS_LIMIT_WARNING_REGISTRATION, &LEGACY_APPLY_PATCH_EXEC_COMMAND_WARNING_REGISTRATION, &LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION, diff --git a/codex-rs/core/src/context/contextual_user_message_tests.rs b/codex-rs/core/src/context/contextual_user_message_tests.rs index 195b189929..dc7cb0f534 100644 --- a/codex-rs/core/src/context/contextual_user_message_tests.rs +++ b/codex-rs/core/src/context/contextual_user_message_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::context::ContextualUserFragment; -use crate::context::GoalContext; +use crate::context::InternalContextSource; +use crate::context::InternalModelContextFragment; use crate::context::SubagentNotification; use codex_protocol::items::HookPromptFragment; use codex_protocol::items::build_hook_prompt_message; @@ -30,23 +31,55 @@ fn detects_subagent_notification_fragment_case_insensitively() { } #[test] -fn detects_goal_context_fragment() { - let text = GoalContext::new("Continue working toward the active thread goal.").render(); +fn detects_internal_model_context_fragment() { + let text = InternalModelContextFragment::new( + InternalContextSource::from_static("goal"), + "Continue working toward the active thread goal.", + ) + .render(); + assert_eq!( + text, + "\nContinue working toward the active thread goal.\n" + ); assert!(is_contextual_user_fragment(&ContentItem::InputText { text })); } +#[test] +fn detects_legacy_goal_context_fragment() { + assert!(is_contextual_user_fragment(&ContentItem::InputText { + text: "\nContinue working toward the active thread goal.\n" + .to_string(), + })); +} + +#[test] +fn does_not_hide_arbitrary_context_tags() { + assert!(!is_contextual_user_fragment(&ContentItem::InputText { + text: "\nbody\n".to_string(), + })); +} + +#[test] +fn rejects_invalid_internal_model_context_source() { + assert!(!is_contextual_user_fragment(&ContentItem::InputText { + text: "\nbody\n" + .to_string(), + })); +} + #[test] fn contextual_user_fragment_is_dyn_compatible() { - let fragment: Box = Box::new(GoalContext::new( + let fragment: Box = Box::new(InternalModelContextFragment::new( + InternalContextSource::from_static("goal"), "Continue working toward the active thread goal.", )); assert_eq!( fragment.render(), - "\nContinue working toward the active thread goal.\n" + "\nContinue working toward the active thread goal.\n" ); } diff --git a/codex-rs/core/src/context/environment_context.rs b/codex-rs/core/src/context/environment_context.rs index a3052b6642..051147b7cd 100644 --- a/codex-rs/core/src/context/environment_context.rs +++ b/codex-rs/core/src/context/environment_context.rs @@ -1,9 +1,17 @@ use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; use crate::shell::Shell; +use codex_protocol::models::ManagedFileSystemPermissions; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; use codex_utils_absolute_path::AbsolutePathBuf; +use std::collections::HashSet; +use std::path::PathBuf; use super::ContextualUserFragment; @@ -13,6 +21,7 @@ pub(crate) struct EnvironmentContext { pub(crate) current_date: Option, pub(crate) timezone: Option, pub(crate) network: Option, + pub(crate) filesystem: Option, pub(crate) subagents: Option, } @@ -83,6 +92,208 @@ impl EnvironmentContextEnvironments { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FileSystemContext { + workspace_roots: Vec, + permission_profile: FileSystemPermissionProfileContext, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum FileSystemPermissionProfileContext { + Managed(ManagedFileSystemContext), + Disabled, + External, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ManagedFileSystemContext { + Restricted { + entries: Vec, + glob_scan_max_depth: Option, + }, + Unrestricted, +} + +impl FileSystemContext { + fn from_permission_profile( + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], + ) -> Self { + let permission_profile = permission_profile + .clone() + .materialize_project_roots_with_workspace_roots(workspace_roots); + let workspace_roots = workspace_roots + .iter() + .map(|root| root.to_string_lossy().into_owned()) + .collect(); + let permission_profile = match permission_profile { + PermissionProfile::Managed { file_system, .. } => { + FileSystemPermissionProfileContext::Managed(ManagedFileSystemContext::from( + file_system, + )) + } + PermissionProfile::Disabled => FileSystemPermissionProfileContext::Disabled, + PermissionProfile::External { .. } => FileSystemPermissionProfileContext::External, + }; + Self { + workspace_roots, + permission_profile, + } + } + + fn render(&self) -> String { + let mut rendered = "".to_string(); + if !self.workspace_roots.is_empty() { + rendered.push_str(""); + for root in &self.workspace_roots { + push_text_element(&mut rendered, "root", root); + } + rendered.push_str(""); + } + self.permission_profile.render(&mut rendered); + rendered.push_str(""); + rendered + } +} + +impl From for ManagedFileSystemContext { + fn from(file_system: ManagedFileSystemPermissions) -> Self { + match file_system { + ManagedFileSystemPermissions::Restricted { + mut entries, + glob_scan_max_depth, + } => { + dedupe_file_system_entries(&mut entries); + Self::Restricted { + entries, + glob_scan_max_depth: glob_scan_max_depth.map(usize::from), + } + } + ManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, + } + } +} + +impl FileSystemPermissionProfileContext { + fn render(&self, rendered: &mut String) { + match self { + Self::Managed(file_system) => { + rendered.push_str(""); + file_system.render(rendered); + rendered.push_str(""); + } + Self::Disabled => { + rendered.push_str( + "", + ); + } + Self::External => { + rendered.push_str( + "", + ); + } + } + } +} + +impl ManagedFileSystemContext { + fn render(&self, rendered: &mut String) { + match self { + Self::Restricted { + entries, + glob_scan_max_depth, + } => { + if entries.is_empty() && glob_scan_max_depth.is_none() { + rendered.push_str(""); + return; + } + + rendered.push_str("'); + for entry in entries { + render_file_system_entry(rendered, entry); + } + rendered.push_str(""); + } + Self::Unrestricted => { + rendered.push_str(""); + } + } + } +} + +fn render_file_system_entry(rendered: &mut String, entry: &FileSystemSandboxEntry) { + rendered.push_str(""); + match &entry.path { + FileSystemPath::Path { path } => { + push_text_element(rendered, "path", path.to_string_lossy().as_ref()); + } + FileSystemPath::GlobPattern { pattern } => { + push_text_element(rendered, "glob", pattern); + } + FileSystemPath::Special { value } => { + let value = render_special_path(value); + push_text_element(rendered, "special", &value); + } + } + rendered.push_str(""); +} + +fn render_special_path(value: &FileSystemSpecialPath) -> String { + match value { + FileSystemSpecialPath::Root => ":root".to_string(), + FileSystemSpecialPath::Minimal => ":minimal".to_string(), + FileSystemSpecialPath::ProjectRoots { subpath } => { + render_special_path_with_subpath(":workspace_roots", subpath) + } + FileSystemSpecialPath::Tmpdir => ":tmpdir".to_string(), + FileSystemSpecialPath::SlashTmp => ":slash_tmp".to_string(), + FileSystemSpecialPath::Unknown { path, subpath } => { + render_special_path_with_subpath(path, subpath) + } + } +} + +fn render_special_path_with_subpath(base: &str, subpath: &Option) -> String { + match subpath { + Some(subpath) => format!("{base}/{}", subpath.display()), + None => base.to_string(), + } +} + +fn dedupe_file_system_entries(entries: &mut Vec) { + let mut seen = HashSet::new(); + entries.retain(|entry| seen.insert(entry.clone())); +} + +fn push_text_element(rendered: &mut String, name: &str, value: &str) { + rendered.push_str(&format!("<{name}>")); + push_xml_escaped_text(rendered, value); + rendered.push_str(&format!("")); +} + +fn push_xml_escaped_text(rendered: &mut String, value: &str) { + for ch in value.chars() { + match ch { + '&' => rendered.push_str("&"), + '<' => rendered.push_str("<"), + '>' => rendered.push_str(">"), + '"' => rendered.push_str("""), + '\'' => rendered.push_str("'"), + _ => rendered.push(ch), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) struct NetworkContext { allowed_domains: Vec, @@ -129,6 +340,7 @@ impl EnvironmentContext { current_date, timezone, network, + filesystem: None, subagents, } } @@ -138,6 +350,7 @@ impl EnvironmentContext { current_date: Option, timezone: Option, network: Option, + filesystem: Option, subagents: Option, ) -> Self { Self { @@ -145,6 +358,7 @@ impl EnvironmentContext { current_date, timezone, network, + filesystem, subagents, } } @@ -157,6 +371,7 @@ impl EnvironmentContext { && self.current_date == other.current_date && self.timezone == other.timezone && self.network == other.network + && self.filesystem == other.filesystem && self.subagents == other.subagents } @@ -165,6 +380,7 @@ impl EnvironmentContext { after: &EnvironmentContext, ) -> Self { let before_network = Self::network_from_turn_context_item(before); + let before_filesystem = Self::filesystem_from_turn_context_item(before); let environments = match &after.environments { EnvironmentContextEnvironments::Single(environment) => { if before.cwd.as_path() != environment.cwd.as_path() { @@ -186,17 +402,23 @@ impl EnvironmentContext { } else { before_network }; + let filesystem = if before_filesystem != after.filesystem { + after.filesystem.clone() + } else { + before_filesystem + }; EnvironmentContext::new_with_environments( environments, after.current_date.clone(), after.timezone.clone(), network, + filesystem, /*subagents*/ None, ) } pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self { - Self::new( + let mut context = Self::new( EnvironmentContextEnvironment::from_turn_environments( &turn_context.environments.turn_environments, shell, @@ -205,7 +427,12 @@ impl EnvironmentContext { turn_context.timezone.clone(), Self::network_from_turn_context(turn_context), /*subagents*/ None, - ) + ); + context.filesystem = Some(FileSystemContext::from_permission_profile( + &turn_context.permission_profile, + &turn_context.config.effective_workspace_roots(), + )); + context } pub(crate) fn from_turn_context_item( @@ -216,11 +443,14 @@ impl EnvironmentContext { Ok(cwd) => cwd, Err(_) => AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/"), }; - Self::new( - vec![EnvironmentContextEnvironment::legacy(cwd, shell)], + Self::new_with_environments( + EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy( + cwd, shell, + )]), turn_context_item.current_date.clone(), turn_context_item.timezone.clone(), Self::network_from_turn_context_item(turn_context_item), + Self::filesystem_from_turn_context_item(turn_context_item), /*subagents*/ None, ) } @@ -266,6 +496,30 @@ impl EnvironmentContext { denied_domains.clone(), )) } + + fn filesystem_from_turn_context_item( + turn_context_item: &TurnContextItem, + ) -> Option { + Some(FileSystemContext::from_permission_profile( + &turn_context_item.permission_profile(), + &workspace_roots_from_turn_context_item(turn_context_item), + )) + } +} + +fn workspace_roots_from_turn_context_item( + turn_context_item: &TurnContextItem, +) -> Vec { + if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() { + return workspace_roots.clone(); + } + + // Older rollout items did not persist workspace roots. Fall back to the + // legacy cwd binding only when reconstructing that historical context. + match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) { + Ok(cwd) => vec![cwd], + Err(_) => Vec::new(), + } } impl ContextualUserFragment for EnvironmentContext { @@ -324,6 +578,9 @@ impl ContextualUserFragment for EnvironmentContext { // lines.push(" ".to_string()); } } + if let Some(filesystem) = &self.filesystem { + lines.push(format!(" {}", filesystem.render())); + } if let Some(subagents) = &self.subagents { lines.push(" ".to_string()); lines.extend(subagents.lines().map(|line| format!(" {line}"))); diff --git a/codex-rs/core/src/context/environment_context_tests.rs b/codex-rs/core/src/context/environment_context_tests.rs index 68ff7c9d44..875ce20c09 100644 --- a/codex-rs/core/src/context/environment_context_tests.rs +++ b/codex-rs/core/src/context/environment_context_tests.rs @@ -1,9 +1,21 @@ use crate::shell::ShellType; use super::*; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::permissions::project_roots_glob_pattern; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::TurnContextItem; use codex_utils_absolute_path::test_support::PathBufExt; use core_test_support::test_path_buf; use pretty_assertions::assert_eq; +use std::path::Path; use std::path::PathBuf; fn fake_shell_name() -> String { @@ -79,6 +91,125 @@ fn serialize_environment_context_with_network() { assert_eq!(context.render(), expected); } +fn workspace_write_permission_profile_with_private_denials() -> PermissionProfile { + PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(PathBuf::from("private"))), + }, + access: FileSystemAccessMode::Deny, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: project_roots_glob_pattern(Path::new("private/**")), + }, + access: FileSystemAccessMode::Deny, + }, + ]), + NetworkSandboxPolicy::Restricted, + ) +} + +#[test] +fn serialize_environment_context_with_full_filesystem_profile() { + let repo = test_abs_path("/repo"); + let other_repo = test_abs_path("/other-repo"); + let repo_private = repo.join("private"); + let other_repo_private = other_repo.join("private"); + let repo_private_glob = + AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), repo.as_path()); + let other_repo_private_glob = + AbsolutePathBuf::resolve_path_against_base(Path::new("private/**"), other_repo.as_path()); + let mut context = EnvironmentContext::new( + vec![EnvironmentContextEnvironment { + id: "local".to_string(), + cwd: test_path_buf("/repo").abs(), + shell: fake_shell_name(), + }], + /*current_date*/ None, + /*timezone*/ None, + /*network*/ None, + /*subagents*/ None, + ); + context.filesystem = Some(FileSystemContext::from_permission_profile( + &workspace_write_permission_profile_with_private_denials(), + &[repo.clone(), other_repo.clone()], + )); + + let expected = format!( + r#" + {} + bash + {repo}{other_repo}{repo}{other_repo}{repo_private}{other_repo_private}{repo_private_glob}{other_repo_private_glob} +"#, + test_path_buf("/repo").display(), + repo = repo.to_string_lossy(), + other_repo = other_repo.to_string_lossy(), + repo_private = repo_private.to_string_lossy(), + other_repo_private = other_repo_private.to_string_lossy(), + repo_private_glob = repo_private_glob.to_string_lossy(), + other_repo_private_glob = other_repo_private_glob.to_string_lossy(), + ); + + assert_eq!(context.render(), expected); +} + +#[test] +fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() { + let repo = test_abs_path("/repo"); + let other_repo = test_abs_path("/other-repo"); + let repo_private = repo.join("private"); + let item = TurnContextItem { + turn_id: None, + cwd: test_path_buf("/not-the-workspace"), + workspace_roots: Some(vec![repo.clone(), other_repo.clone()]), + current_date: None, + timezone: None, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: Some(workspace_write_permission_profile_with_private_denials()), + network: None, + file_system_sandbox_policy: None, + model: "gpt-5".to_string(), + personality: None, + collaboration_mode: None, + realtime_active: None, + effort: None, + summary: codex_protocol::config_types::ReasoningSummary::Auto, + }; + + let context = EnvironmentContext::from_turn_context_item(&item, fake_shell_name()).render(); + + assert!( + context.contains(&format!( + "{}{}", + repo.to_string_lossy(), + other_repo.to_string_lossy() + )), + "{context}" + ); + assert!( + context.contains(&format!("{}", repo_private.to_string_lossy())), + "{context}" + ); + assert!( + !context.contains( + test_abs_path("/not-the-workspace") + .join("private") + .to_string_lossy() + .as_ref() + ), + "{context}" + ); +} + #[test] fn serialize_read_only_environment_context() { let context = EnvironmentContext::new( diff --git a/codex-rs/core/src/context/fragment.rs b/codex-rs/core/src/context/fragment.rs index 4a9e284d12..5c500fe8ef 100644 --- a/codex-rs/core/src/context/fragment.rs +++ b/codex-rs/core/src/context/fragment.rs @@ -1,4 +1,5 @@ use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use std::marker::PhantomData; @@ -81,6 +82,19 @@ pub trait ContextualUserFragment { phase: None, } } + + fn into_response_input_item(self) -> ResponseInputItem + where + Self: Sized, + { + ResponseInputItem::Message { + role: Self::role().to_string(), + content: vec![ContentItem::InputText { + text: self.render(), + }], + phase: None, + } + } } fn matches_marked_text(start_marker: &str, end_marker: &str, text: &str) -> bool { diff --git a/codex-rs/core/src/context/fragments.rs b/codex-rs/core/src/context/fragments.rs new file mode 100644 index 0000000000..e23f96f59f --- /dev/null +++ b/codex-rs/core/src/context/fragments.rs @@ -0,0 +1,92 @@ +use super::ContextualUserFragment; +use codex_utils_string::truncate_middle_with_token_budget; + +const MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS: usize = 1_000; +const ADDITIONAL_CONTEXT_END_MARKER_SUFFIX: &str = ">"; +const ADDITIONAL_CONTEXT_START_MARKER_PREFIX: &str = " Self { + Self { key, value } + } +} + +impl ContextualUserFragment for AdditionalContextUserFragment { + fn role() -> &'static str { + "user" + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ( + ADDITIONAL_CONTEXT_START_MARKER_PREFIX, + ADDITIONAL_CONTEXT_END_MARKER_SUFFIX, + ) + } + + fn matches_text(text: &str) -> bool { + let trimmed = text.trim(); + let Some(rest) = trimmed.strip_prefix(ADDITIONAL_CONTEXT_START_MARKER_PREFIX) else { + return false; + }; + let Some((key, value_and_close)) = rest.split_once(ADDITIONAL_CONTEXT_END_MARKER_SUFFIX) + else { + return false; + }; + + value_and_close.ends_with(&format!("")) + } + + fn body(&self) -> String { + additional_context_body(&self.key, &self.value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AdditionalContextDeveloperFragment { + key: String, + value: String, +} + +impl AdditionalContextDeveloperFragment { + pub(crate) fn new(key: String, value: String) -> Self { + Self { key, value } + } +} + +impl ContextualUserFragment for AdditionalContextDeveloperFragment { + fn role() -> &'static str { + "developer" + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ("", "") + } + + fn body(&self) -> String { + additional_context_developer_body(&self.key, &self.value) + } +} + +fn additional_context_body(key: &str, value: &str) -> String { + let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0; + format!("{key}>{value} String { + let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0; + format!("<{key}>{value}") +} diff --git a/codex-rs/core/src/context/goal_context.rs b/codex-rs/core/src/context/goal_context.rs deleted file mode 100644 index 13892bf466..0000000000 --- a/codex-rs/core/src/context/goal_context.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Hidden user-context fragment for runtime-owned goal steering prompts. - -use super::ContextualUserFragment; -use codex_protocol::models::ContentItem; -use codex_protocol::models::ResponseInputItem; - -/// Hidden runtime-owned goal steering context injected into model input. -#[derive(Debug, Clone, PartialEq)] -pub struct GoalContext { - prompt: String, -} - -impl GoalContext { - /// Creates goal context around an already-rendered steering prompt. - pub fn new(prompt: impl Into) -> Self { - Self { - prompt: prompt.into(), - } - } - - /// Converts the registered fragment into an active-turn injectable item. - pub fn into_response_input_item(self) -> ResponseInputItem { - ResponseInputItem::Message { - role: ::role().to_string(), - content: vec![ContentItem::InputText { - text: self.render(), - }], - phase: None, - } - } -} - -impl ContextualUserFragment for GoalContext { - fn role() -> &'static str { - "user" - } - - fn markers(&self) -> (&'static str, &'static str) { - Self::type_markers() - } - - fn type_markers() -> (&'static str, &'static str) { - ("", "") - } - - fn body(&self) -> String { - format!("\n{}\n", self.prompt) - } -} diff --git a/codex-rs/core/src/context/internal_model_context.rs b/codex-rs/core/src/context/internal_model_context.rs new file mode 100644 index 0000000000..a25e77a82a --- /dev/null +++ b/codex-rs/core/src/context/internal_model_context.rs @@ -0,0 +1,129 @@ +//! Hidden user-context fragment for extension-owned model steering. + +use super::ContextualUserFragment; +use std::error::Error; +use std::fmt; + +const CONTEXT_START_MARKER: &str = ""; + +/// Source label for hidden internal model context. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InternalContextSource(String); + +impl InternalContextSource { + /// Creates a source label for an internal model-context fragment. + /// + /// Sources are intentionally constrained so the value can be embedded in the + /// wrapper without escaping and still remain easy to audit in stored history. + pub fn new(source: impl Into) -> Result { + let source = source.into(); + if is_valid_source(&source) { + Ok(Self(source)) + } else { + Err(InvalidInternalContextSource { source }) + } + } + + /// Creates a source label from a trusted static string. + pub fn from_static(source: &'static str) -> Self { + Self::new(source) + .unwrap_or_else(|_| panic!("invalid static internal context source: {source}")) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Error returned when an internal model-context source is invalid. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InvalidInternalContextSource { + source: String, +} + +impl fmt::Display for InvalidInternalContextSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let source = &self.source; + write!( + f, + "invalid internal model context source {source:?}; expected [a-z][a-z0-9_]*" + ) + } +} + +impl Error for InvalidInternalContextSource {} + +/// Hidden runtime-owned context injected into model input. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InternalModelContextFragment { + source: InternalContextSource, + body: String, +} + +impl InternalModelContextFragment { + /// Creates hidden model context with an extension-owned source label. + pub fn new(source: InternalContextSource, body: impl Into) -> Self { + Self { + source, + body: body.into(), + } + } +} + +impl ContextualUserFragment for InternalModelContextFragment { + fn role() -> &'static str { + "user" + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + (CONTEXT_START_MARKER, CONTEXT_END_MARKER) + } + + fn matches_text(text: &str) -> bool { + let trimmed = text.trim(); + if matches_legacy_goal_context(trimmed) { + return true; + } + + let Some(rest) = trimmed.strip_prefix(CONTEXT_START_MARKER) else { + return false; + }; + let Some(rest) = rest.strip_prefix(SOURCE_ATTR_START) else { + return false; + }; + let Some((source, body_and_close)) = rest.split_once(SOURCE_ATTR_END) else { + return false; + }; + + is_valid_source(source) && body_and_close.ends_with(CONTEXT_END_MARKER) + } + + fn body(&self) -> String { + let source = self.source.as_str(); + let body = &self.body; + format!(" source=\"{source}\">\n{body}\n") + } +} + +fn matches_legacy_goal_context(text: &str) -> bool { + text.starts_with(LEGACY_GOAL_CONTEXT_START_MARKER) + && text.ends_with(LEGACY_GOAL_CONTEXT_END_MARKER) +} + +fn is_valid_source(source: &str) -> bool { + let mut chars = source.chars(); + let Some(first) = chars.next() else { + return false; + }; + first.is_ascii_lowercase() + && chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_') +} diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index 99575f6309..3e01d5212e 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -8,10 +8,11 @@ mod collaboration_mode_instructions; mod contextual_user_message; mod environment_context; mod fragment; -mod goal_context; +mod fragments; mod guardian_followup_review_reminder; mod hook_additional_context; mod image_generation_instructions; +mod internal_model_context; mod legacy_apply_patch_exec_command_warning; mod legacy_model_mismatch_warning; mod legacy_unified_exec_process_limit_warning; @@ -40,10 +41,14 @@ pub(crate) use environment_context::EnvironmentContext; pub use fragment::ContextualUserFragment; pub(crate) use fragment::FragmentRegistration; pub(crate) use fragment::FragmentRegistrationProxy; -pub use goal_context::GoalContext; +pub(crate) use fragments::AdditionalContextDeveloperFragment; +pub(crate) use fragments::AdditionalContextUserFragment; pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder; pub(crate) use hook_additional_context::HookAdditionalContext; pub(crate) use image_generation_instructions::ImageGenerationInstructions; +pub use internal_model_context::InternalContextSource; +pub use internal_model_context::InternalModelContextFragment; +pub use internal_model_context::InvalidInternalContextSource; pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecCommandWarning; pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning; pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning; diff --git a/codex-rs/core/src/context/permissions_instructions.rs b/codex-rs/core/src/context/permissions_instructions.rs index db8f983c16..513390d592 100644 --- a/codex-rs/core/src/context/permissions_instructions.rs +++ b/codex-rs/core/src/context/permissions_instructions.rs @@ -1,148 +1,6 @@ use super::ContextualUserFragment; -use codex_execpolicy::Policy; -use codex_protocol::config_types::ApprovalsReviewer; -use codex_protocol::config_types::SandboxMode; -use codex_protocol::models::PermissionProfile; -use codex_protocol::models::format_allow_prefixes; -use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::GranularApprovalConfig; -use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::WritableRoot; -use codex_utils_template::Template; -use std::path::Path; -use std::sync::LazyLock; -const APPROVAL_POLICY_NEVER: &str = include_str!("prompts/permissions/approval_policy/never.md"); -const APPROVAL_POLICY_UNLESS_TRUSTED: &str = - include_str!("prompts/permissions/approval_policy/unless_trusted.md"); -const APPROVAL_POLICY_ON_FAILURE: &str = - include_str!("prompts/permissions/approval_policy/on_failure.md"); -const APPROVAL_POLICY_ON_REQUEST_RULE: &str = - include_str!("prompts/permissions/approval_policy/on_request.md"); -const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str = - include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md"); -const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval."; - -const SANDBOX_MODE_DANGER_FULL_ACCESS: &str = - include_str!("prompts/permissions/sandbox_mode/danger_full_access.md"); -const SANDBOX_MODE_WORKSPACE_WRITE: &str = - include_str!("prompts/permissions/sandbox_mode/workspace_write.md"); -const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md"); - -static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock