diff --git a/.bazelrc b/.bazelrc
index 3a49a22c2..085bfc8a7 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -137,6 +137,16 @@ common:ci-linux --config=remote
common:ci-linux --strategy=remote
common:ci-linux --platforms=//:rbe
+# Fork-only Linux jobs that must stay on Bazel but cannot rely on BuildBuddy
+# remote execution should use this explicit local variant instead of trying to
+# partially undo `ci-linux` at the callsite.
+common:ci-linux-local --config=ci-bazel
+common:ci-linux-local --build_metadata=TAG_os=linux
+common:ci-linux-local --extra_execution_platforms=
+common:ci-linux-local --platforms=//:local_linux
+common:ci-linux-local --spawn_strategy=local
+common:ci-linux-local --strategy=local
+
# On mac, we can run all the build actions remotely but test actions locally.
common:ci-macos --config=ci-bazel
common:ci-macos --build_metadata=TAG_os=macos
diff --git a/.github/scripts/run-bazel-ci.sh b/.github/scripts/run-bazel-ci.sh
index a1d42a057..ed6706681 100755
--- a/.github/scripts/run-bazel-ci.sh
+++ b/.github/scripts/run-bazel-ci.sh
@@ -65,6 +65,11 @@ case "${RUNNER_OS:-}" in
;;
esac
+force_local_ci_config=
+if [[ "${RUNNER_OS:-}" == "Linux" ]]; then
+ force_local_ci_config=ci-linux-local
+fi
+
print_bazel_test_log_tails() {
local console_log="$1"
local testlogs_dir
@@ -241,7 +246,29 @@ if (( ${#bazel_startup_args[@]} > 0 )); then
bazel_cmd+=("${bazel_startup_args[@]}")
fi
-if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
+if [[ "${CODEX_BAZEL_FORCE_LOCAL:-0}" == "1" ]]; then
+ echo "CODEX_BAZEL_FORCE_LOCAL=1; using local Bazel configuration."
+ bazel_run_args=(
+ "${bazel_args[@]}"
+ --remote_cache=
+ --remote_executor=
+ )
+ if [[ -n "${force_local_ci_config}" ]]; then
+ bazel_run_args+=("--config=${force_local_ci_config}")
+ fi
+ if (( ${#post_config_bazel_args[@]} > 0 )); then
+ bazel_run_args+=("${post_config_bazel_args[@]}")
+ fi
+ set +e
+ run_bazel "${bazel_cmd[@]:1}" \
+ --noexperimental_remote_repo_contents_cache \
+ "${bazel_run_args[@]}" \
+ -- \
+ "${bazel_targets[@]}" \
+ 2>&1 | tee "$bazel_console_log"
+ bazel_status=${PIPESTATUS[0]}
+ set -e
+elif [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
echo "BuildBuddy API key is available; using remote Bazel configuration."
# Work around Bazel 9 remote repo contents cache / overlay materialization failures
# seen in CI (for example "is not a symlink" or permission errors while
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index d14817f00..66a92a5e8 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -1,6 +1,7 @@
# Workflow Strategy
-The workflows in this directory are split so that pull requests get fast, review-friendly signal while `main` still gets the full cross-platform verification pass.
+This fork keeps pushes to `main` quiet. Heavier validation runs stay available through
+`workflow_dispatch`, while pull requests can still use targeted review-time checks.
## Pull Requests
@@ -14,18 +15,23 @@ The workflows in this directory are split so that pull requests get fast, review
- `argument-comment-lint` on Linux, macOS, and Windows
- `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
-## Post-Merge On `main`
+## Manual Verification
-- `bazel.yml` also runs on pushes to `main`.
- This re-verifies the merged Bazel path and helps keep the BuildBuddy caches warm.
+- `bazel.yml` is available as a manual verification path when the fork needs a full
+ Bazel pass.
- `rust-ci-full.yml` is the full Cargo-native verification workflow.
- It keeps the heavier checks off the PR path while still validating them after merge:
+ It keeps the heavier checks off the PR path while still providing an on-demand
+ validation path:
- the full Cargo `clippy` matrix
- the full Cargo `nextest` matrix
- release-profile Cargo builds
- cross-platform `argument-comment-lint`
- Linux remote-env tests
+Other repo-level checks that used to run on `push(main)` in upstream, such as
+`ci.yml`, `cargo-deny.yml`, `codespell.yml`, `sdk.yml`, and `v8-canary.yml`, are also
+manual-only in this fork so routine sync pushes do not fan out into unrelated CI.
+
## Rule Of Thumb
- If a build/test/clippy check can be expressed in Bazel, prefer putting the PR-time version in `bazel.yml`.
diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 0328ac207..90780a7ae 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -5,9 +5,6 @@ name: Bazel
on:
pull_request: {}
- push:
- branches:
- - main
workflow_dispatch:
concurrency:
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index 5294d0c7c..b118046b5 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -2,9 +2,7 @@ name: cargo-deny
on:
pull_request:
- push:
- branches:
- - main
+ workflow_dispatch:
jobs:
cargo-deny:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8c745106c..c60e56c3a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,7 @@ name: ci
on:
pull_request: {}
- push: { branches: [main] }
+ workflow_dispatch:
jobs:
build-test:
@@ -40,6 +40,7 @@ jobs:
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Stage npm package
+ if: ${{ github.repository == 'openai/codex' }}
id: stage_npm_package
env:
GH_TOKEN: ${{ github.token }}
@@ -56,6 +57,7 @@ jobs:
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
- name: Upload staged npm package artifact
+ if: ${{ github.repository == 'openai/codex' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: codex-npm-staging
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 8e9f701ee..7f6e88108 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -3,10 +3,9 @@
name: Codespell
on:
- push:
- branches: [main]
pull_request:
branches: [main]
+ workflow_dispatch:
permissions:
contents: read
@@ -20,7 +19,7 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Annotate locations with typos
- uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
+ uses: codespell-project/codespell-problem-matcher@9ba2c57125d4908eade4308f32c4ff814c184633 # v1.2.0
- name: Codespell
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
with:
diff --git a/.github/workflows/pypi-release-artifacts.yml b/.github/workflows/pypi-release-artifacts.yml
new file mode 100644
index 000000000..ea176a2ed
--- /dev/null
+++ b/.github/workflows/pypi-release-artifacts.yml
@@ -0,0 +1,165 @@
+name: pypi-release-artifacts
+
+on:
+ workflow_call:
+ inputs:
+ checkout_ref:
+ required: true
+ type: string
+ release_tag:
+ required: true
+ type: string
+
+concurrency:
+ group: pypi-release-artifacts-${{ inputs.release_tag }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: build-artifact-${{ matrix.target }}
+ runs-on: ${{ matrix.runner }}
+ permissions:
+ contents: read
+ env:
+ CARGO_INCREMENTAL: "0"
+ RUSTC_WRAPPER: ""
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: macos-14
+ target: aarch64-apple-darwin
+ runtime_binary_name: codex
+ release_asset_name: codex-aarch64-apple-darwin.tar.gz
+ build_args: --bin codex
+ - runner: macos-15-intel
+ target: x86_64-apple-darwin
+ runtime_binary_name: codex
+ release_asset_name: codex-x86_64-apple-darwin.tar.gz
+ build_args: --bin codex
+ - runner: ubuntu-24.04
+ target: x86_64-unknown-linux-gnu
+ runtime_binary_name: codex
+ release_asset_name: codex-x86_64-unknown-linux-gnu.tar.gz
+ build_args: --bin codex
+ - runner: windows-2022
+ target: x86_64-pc-windows-msvc
+ runtime_binary_name: codex.exe
+ release_asset_name: codex-x86_64-pc-windows-msvc.zip
+ build_args: --bin codex --bin codex-windows-sandbox-setup --bin codex-command-runner
+ steps:
+ - name: Checkout release ref
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.checkout_ref }}
+
+ - uses: dtolnay/rust-toolchain@1.93.0
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Clear workspace build rustflags for CI release builds
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
+
+ - name: Use default macOS linker
+ if: ${{ runner.os == 'macOS' }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ echo "RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS=" >> "$GITHUB_ENV"
+ echo "CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS=" >> "$GITHUB_ENV"
+
+ - name: Install Linux build dependencies
+ if: ${{ runner.os == 'Linux' }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ sudo apt-get update
+ sudo apt-get install -y libcap-dev pkg-config protobuf-compiler
+
+ - name: Install macOS build dependencies
+ if: ${{ runner.os == 'macOS' }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ if ! command -v protoc >/dev/null 2>&1; then
+ brew install protobuf
+ fi
+
+ - name: Install Windows build dependencies
+ if: ${{ runner.os == 'Windows' }}
+ shell: pwsh
+ run: |
+ if (-not (Get-Command protoc -ErrorAction SilentlyContinue)) {
+ choco install protoc -y --no-progress
+ }
+
+ - name: Build release binaries
+ shell: bash
+ working-directory: codex-rs
+ run: |
+ set -euo pipefail
+ cargo build --locked --release --target "${{ matrix.target }}" ${{ matrix.build_args }}
+
+ - name: Stage runtime artifact
+ shell: bash
+ run: |
+ set -euo pipefail
+ mkdir -p "${RUNNER_TEMP}/runtime-artifact"
+ cp "codex-rs/target/${{ matrix.target }}/release/${{ matrix.runtime_binary_name }}" \
+ "${RUNNER_TEMP}/runtime-artifact/${{ matrix.runtime_binary_name }}"
+
+ - name: Stage release asset
+ shell: bash
+ run: |
+ set -euo pipefail
+ release_dir="${RUNNER_TEMP}/release-asset"
+ mkdir -p "${release_dir}"
+
+ if [[ "${{ runner.os }}" == "Windows" ]]; then
+ cp "codex-rs/target/${{ matrix.target }}/release/codex.exe" "${release_dir}/codex.exe"
+ cp "codex-rs/target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe" "${release_dir}/codex-windows-sandbox-setup.exe"
+ cp "codex-rs/target/${{ matrix.target }}/release/codex-command-runner.exe" "${release_dir}/codex-command-runner.exe"
+ else
+ cp "codex-rs/target/${{ matrix.target }}/release/codex" "${release_dir}/codex"
+ fi
+
+ - name: Archive Unix release asset
+ if: ${{ runner.os != 'Windows' }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ release_dir="${RUNNER_TEMP}/release-asset"
+ archive_dir="${RUNNER_TEMP}/release-archive"
+ mkdir -p "${archive_dir}"
+ tar -C "${release_dir}" -czf "${archive_dir}/${{ matrix.release_asset_name }}" codex
+
+ - name: Archive Windows release asset
+ if: ${{ runner.os == 'Windows' }}
+ shell: pwsh
+ run: |
+ $releaseDir = Join-Path $env:RUNNER_TEMP "release-asset"
+ $archiveDir = Join-Path $env:RUNNER_TEMP "release-archive"
+ New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
+ Compress-Archive -Path (Join-Path $releaseDir '*') -DestinationPath (Join-Path $archiveDir '${{ matrix.release_asset_name }}') -Force
+
+ - name: Upload runtime binary artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: pypi-runtime-${{ matrix.target }}
+ path: ${{ runner.temp }}/runtime-artifact/*
+ if-no-files-found: error
+
+ - name: Upload GitHub release asset
+ uses: actions/upload-artifact@v4
+ with:
+ name: pypi-release-asset-${{ matrix.target }}
+ path: ${{ runner.temp }}/release-archive/*
+ if-no-files-found: error
diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml
new file mode 100644
index 000000000..c182054ce
--- /dev/null
+++ b/.github/workflows/pypi-release.yml
@@ -0,0 +1,278 @@
+name: pypi-release
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+ workflow_dispatch:
+ inputs:
+ release_tag:
+ description: Existing tag to build and publish, for example v0.1.12
+ required: true
+ type: string
+ artifact_run_id:
+ description: Existing pypi-release run id whose wheel and release artifacts should be reused
+ required: false
+ type: string
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref_name || inputs.release_tag }}
+ cancel-in-progress: true
+
+jobs:
+ prepare:
+ runs-on: ubuntu-latest
+ outputs:
+ release_tag: ${{ steps.meta.outputs.release_tag }}
+ release_version: ${{ steps.meta.outputs.release_version }}
+ checkout_ref: ${{ steps.meta.outputs.checkout_ref }}
+ artifact_run_id: ${{ steps.meta.outputs.artifact_run_id }}
+ reuse_artifacts: ${{ steps.meta.outputs.reuse_artifacts }}
+ steps:
+ - name: Resolve release metadata
+ id: meta
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
+ release_tag="${{ inputs.release_tag }}"
+ checkout_ref="refs/tags/${release_tag}"
+ else
+ release_tag="${GITHUB_REF_NAME}"
+ checkout_ref="${GITHUB_REF}"
+ fi
+
+ [[ "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \
+ || { echo "Release tag ${release_tag} is not in the expected format."; exit 1; }
+
+ echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
+ echo "release_version=${release_tag#v}" >> "$GITHUB_OUTPUT"
+ echo "checkout_ref=${checkout_ref}" >> "$GITHUB_OUTPUT"
+ artifact_run_id="${{ inputs.artifact_run_id }}"
+ if [[ -n "${artifact_run_id}" ]]; then
+ echo "artifact_run_id=${artifact_run_id}" >> "$GITHUB_OUTPUT"
+ echo "reuse_artifacts=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "artifact_run_id=" >> "$GITHUB_OUTPUT"
+ echo "reuse_artifacts=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Checkout release ref
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.meta.outputs.checkout_ref }}
+
+ - name: Validate tag matches codex-enhanced runtime version
+ shell: bash
+ run: |
+ set -euo pipefail
+ runtime_ver="$(grep -m1 '^version' sdk/python-runtime-enhanced/pyproject.toml | sed -E 's/version *= *"([^"]+)".*/\1/')"
+ tag_ver="${{ steps.meta.outputs.release_version }}"
+ [[ "${tag_ver}" == "${runtime_ver}" ]] \
+ || { echo "Tag version ${tag_ver} does not match sdk/python-runtime-enhanced ${runtime_ver}."; exit 1; }
+
+ release-assets:
+ needs: prepare
+ if: ${{ needs.prepare.outputs.reuse_artifacts != 'true' }}
+ uses: ./.github/workflows/pypi-release-artifacts.yml
+ with:
+ checkout_ref: ${{ needs.prepare.outputs.checkout_ref }}
+ release_tag: ${{ needs.prepare.outputs.release_tag }}
+
+ build:
+ needs:
+ - prepare
+ - release-assets
+ name: build-wheel-${{ matrix.target }}
+ if: ${{ needs.prepare.outputs.reuse_artifacts != 'true' && needs.release-assets.result == 'success' }}
+ runs-on: ${{ matrix.runner }}
+ permissions:
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: macos-14
+ target: aarch64-apple-darwin
+ runtime_artifact_name: pypi-runtime-aarch64-apple-darwin
+ binary_name: codex
+ - runner: macos-15-intel
+ target: x86_64-apple-darwin
+ runtime_artifact_name: pypi-runtime-x86_64-apple-darwin
+ binary_name: codex
+ - runner: ubuntu-24.04
+ target: x86_64-unknown-linux-gnu
+ runtime_artifact_name: pypi-runtime-x86_64-unknown-linux-gnu
+ binary_name: codex
+ - runner: windows-2022
+ target: x86_64-pc-windows-msvc
+ runtime_artifact_name: pypi-runtime-x86_64-pc-windows-msvc
+ binary_name: codex.exe
+ steps:
+ - name: Checkout release ref
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.prepare.outputs.checkout_ref }}
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+
+ - name: Install Python build dependencies
+ shell: bash
+ run: python -m pip install --upgrade build hatchling packaging
+
+ - name: Download release binary artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: ${{ matrix.runtime_artifact_name }}
+ path: ${{ runner.temp }}/runtime-artifact
+
+ - name: Stage codex-enhanced runtime package
+ shell: bash
+ run: |
+ set -euo pipefail
+ python sdk/python/scripts/update_sdk_artifacts.py \
+ stage-runtime \
+ "${RUNNER_TEMP}/codex-enhanced" \
+ "${RUNNER_TEMP}/runtime-artifact/${{ matrix.binary_name }}" \
+ --runtime-version "${{ needs.prepare.outputs.release_version }}" \
+ --runtime-package enhanced
+
+ - name: Build wheel
+ shell: bash
+ run: python -m build --wheel "${RUNNER_TEMP}/codex-enhanced"
+
+ - name: Upload wheel artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: pypi-wheel-${{ matrix.target }}
+ path: ${{ runner.temp }}/codex-enhanced/dist/*
+ if-no-files-found: error
+
+ publish:
+ needs:
+ - prepare
+ - release-assets
+ - build
+ if: ${{ always() && needs.prepare.result == 'success' && ((needs.prepare.outputs.reuse_artifacts == 'true' && needs.build.result == 'skipped') || (needs.prepare.outputs.reuse_artifacts != 'true' && needs.release-assets.result == 'success' && needs.build.result == 'success')) }}
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ id-token: write
+ steps:
+ - name: Download wheel artifacts from current run
+ if: ${{ needs.prepare.outputs.reuse_artifacts != 'true' }}
+ uses: actions/download-artifact@v4
+ with:
+ pattern: pypi-wheel-*
+ path: dist
+ merge-multiple: true
+
+ - name: Download wheel artifacts from an earlier run
+ if: ${{ needs.prepare.outputs.reuse_artifacts == 'true' }}
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ mkdir -p dist
+ work_dir="$(mktemp -d)"
+ gh run download "${{ needs.prepare.outputs.artifact_run_id }}" \
+ --repo "${GITHUB_REPOSITORY}" \
+ --dir "${work_dir}"
+ find "${work_dir}" -type f -name '*.whl' -exec mv {} dist/ \;
+ if ! find dist -maxdepth 1 -type f -name '*.whl' | grep -q .; then
+ echo "No wheel artifacts were downloaded from run ${{ needs.prepare.outputs.artifact_run_id }}."
+ exit 1
+ fi
+
+ - name: Publish codex-enhanced wheels to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist
+ skip-existing: true
+ verbose: true
+
+ github-release:
+ needs:
+ - prepare
+ - release-assets
+ - build
+ - publish
+ if: ${{ always() && needs.prepare.result == 'success' && needs.publish.result == 'success' && ((needs.prepare.outputs.reuse_artifacts == 'true' && needs.release-assets.result == 'skipped' && needs.build.result == 'skipped') || (needs.prepare.outputs.reuse_artifacts != 'true' && needs.release-assets.result == 'success' && needs.build.result == 'success')) }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout release ref
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.prepare.outputs.checkout_ref }}
+
+ - name: Download release assets from current run
+ if: ${{ needs.prepare.outputs.reuse_artifacts != 'true' }}
+ uses: actions/download-artifact@v4
+ with:
+ pattern: pypi-release-asset-*
+ path: release-dist
+ merge-multiple: true
+
+ - name: Download wheel artifacts from current run
+ if: ${{ needs.prepare.outputs.reuse_artifacts != 'true' }}
+ uses: actions/download-artifact@v4
+ with:
+ pattern: pypi-wheel-*
+ path: release-dist
+ merge-multiple: true
+
+ - name: Download release assets and wheels from an earlier run
+ if: ${{ needs.prepare.outputs.reuse_artifacts == 'true' }}
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ mkdir -p release-dist
+ work_dir="$(mktemp -d)"
+ gh run download "${{ needs.prepare.outputs.artifact_run_id }}" \
+ --repo "${GITHUB_REPOSITORY}" \
+ --dir "${work_dir}"
+ find "${work_dir}" -type f \( -name '*.whl' -o -name '*.tar.gz' -o -name '*.zip' \) -exec mv {} release-dist/ \;
+ if ! find release-dist -maxdepth 1 -type f | grep -q .; then
+ echo "No release assets were downloaded from run ${{ needs.prepare.outputs.artifact_run_id }}."
+ exit 1
+ fi
+
+ - name: Generate GitHub Release notes
+ shell: bash
+ env:
+ RELEASE_VERSION: ${{ needs.prepare.outputs.release_version }}
+ run: |
+ set -euo pipefail
+ commit="$(git rev-parse HEAD^{commit})"
+ notes_path="${RUNNER_TEMP}/release-notes.md"
+ pypi_url="https://pypi.org/project/codex-enhanced/${{ needs.prepare.outputs.release_version }}/"
+
+ git log -1 --format=%B "${commit}" > "${notes_path}"
+ echo >> "${notes_path}"
+ {
+ echo "## PyPI"
+ echo
+ echo "- https://pypi.org/project/codex-enhanced/"
+ echo "- ${pypi_url}"
+ } >> "${notes_path}"
+
+ echo "RELEASE_NOTES_PATH=${notes_path}" >> "$GITHUB_ENV"
+
+ - name: Publish GitHub Release
+ uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
+ with:
+ name: ${{ needs.prepare.outputs.release_version }}
+ tag_name: ${{ needs.prepare.outputs.release_tag }}
+ body_path: ${{ env.RELEASE_NOTES_PATH }}
+ files: release-dist/*
+ overwrite_files: true
+ prerelease: ${{ contains(needs.prepare.outputs.release_version, '-') }}
diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml
index 7cda68295..cf556e87a 100644
--- a/.github/workflows/rust-ci-full.yml
+++ b/.github/workflows/rust-ci-full.yml
@@ -2,7 +2,6 @@ name: rust-ci-full
on:
push:
branches:
- - main
- "**full-ci**"
workflow_dispatch:
diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index 4da750b7e..962a11b79 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -140,19 +140,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- include:
- - name: Linux
- runner: ubuntu-24.04
- timeout_minutes: 30
- - name: macOS
- runner: macos-15-xlarge
- timeout_minutes: 30
- - name: Windows
- runner: windows-x64
- timeout_minutes: 30
- runs_on:
- group: codex-runners
- labels: codex-windows-x64
+ include: ${{ fromJSON(github.repository == 'openai/codex' && '[{"name":"Linux","runner":"ubuntu-24.04","timeout_minutes":30},{"name":"macOS","runner":"macos-15-xlarge","timeout_minutes":30},{"name":"Windows","runner":"windows-x64","timeout_minutes":30,"runs_on":{"group":"codex-runners","labels":"codex-windows-x64"}}]' || '[{"name":"Linux","runner":"ubuntu-24.04","timeout_minutes":120}]') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: ./.github/actions/setup-bazel-ci
@@ -164,9 +152,60 @@ jobs:
shell: bash
run: |
sudo DEBIAN_FRONTEND=noninteractive apt-get update
- sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
+ sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev protobuf-compiler libprotobuf-dev
+ - name: Run argument comment lint on codex-rs via prebuilt wrapper
+ if: ${{ runner.os == 'Linux' && github.repository != 'openai/codex' }}
+ shell: bash
+ run: |
+ heartbeat() {
+ while true; do
+ sleep 60
+ echo "argument comment lint still running via prebuilt wrapper ($(date -u '+%Y-%m-%dT%H:%M:%SZ'))"
+ done
+ }
+
+ heartbeat &
+ heartbeat_pid=$!
+ cleanup() {
+ kill "$heartbeat_pid" 2>/dev/null || true
+ }
+ trap cleanup EXIT
+
+ rustup toolchain install nightly-2025-09-18 \
+ --profile minimal \
+ --component llvm-tools-preview \
+ --component rustc-dev \
+ --component rust-src \
+ --no-self-update
+
+ set +e
+ timeout --foreground --signal=TERM --kill-after=30s 110m \
+ ./tools/argument-comment-lint/run-prebuilt-linter.py
+ status=$?
+ set -e
+
+ if [[ $status -eq 124 ]]; then
+ echo "argument comment lint prebuilt-wrapper step exceeded 110 minutes; failing explicitly before the 120-minute job timeout so shared CI follow-up can diagnose it."
+ fi
+
+ exit "$status"
+ - name: Run argument comment lint on codex-rs via Bazel
+ if: ${{ runner.os == 'Linux' && github.repository == 'openai/codex' }}
+ env:
+ BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
+ shell: bash
+ run: |
+ bazel_targets="$(./tools/argument-comment-lint/list-bazel-targets.sh)"
+ ./.github/scripts/run-bazel-ci.sh \
+ -- \
+ build \
+ --config=argument-comment-lint \
+ --keep_going \
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
+ -- \
+ ${bazel_targets}
- name: Run argument comment lint on codex-rs via Bazel
- if: ${{ runner.os != 'Windows' }}
+ if: ${{ runner.os == 'macOS' }}
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
shell: bash
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index 45c983ac1..0da6755fc 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -1,9 +1,8 @@
name: sdk
on:
- push:
- branches: [main]
pull_request: {}
+ workflow_dispatch:
jobs:
sdks:
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index f5aa1d7c6..05d02bf58 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -12,19 +12,6 @@ on:
- "patches/BUILD.bazel"
- "patches/v8_*.patch"
- "third_party/v8/**"
- push:
- branches:
- - main
- paths:
- - ".github/scripts/rusty_v8_bazel.py"
- - ".github/workflows/rusty-v8-release.yml"
- - ".github/workflows/v8-canary.yml"
- - "MODULE.bazel"
- - "MODULE.bazel.lock"
- - "codex-rs/Cargo.toml"
- - "patches/BUILD.bazel"
- - "patches/v8_*.patch"
- - "third_party/v8/**"
workflow_dispatch:
concurrency:
diff --git a/.gitignore b/.gitignore
index 82269594b..4ecaeca37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,3 +92,10 @@ CHANGELOG.ignore.md
__pycache__/
*.pyc
+.codex/
+.desloppify/
+scorecard.png
+site/
+docs/assets/
+# CocoIndex Code (ccc)
+/.cocoindex_code/
diff --git a/AGENTS.md b/AGENTS.md
index 6e21be78f..5ba0015cc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,8 @@ In the codex-rs folder where the rust code lives:
- Use an exact `/*param_name*/` comment before opaque literal arguments such as `None`, booleans, and numeric literals when passing them by position.
- Do not add these comments for string or char literals unless the comment adds real clarity; those literals are intentionally exempt from the lint.
- The parameter name in the comment must exactly match the callee signature.
- - You can run `just argument-comment-lint` to run the lint check locally. This is powered by Bazel, so running it the first time can be slow if Bazel is not warmed up, though incremental invocations should take <15s. Most of the time, it is best to update the PR and let CI take responsibility for checking this (or run it asynchronously in the background after submitting the PR). Note CI checks all three platforms, which the local run does not.
+ - Do not run bazel related commands unless in CI environement
+ - Do not run 'just argument-comment-lint' unless in CI environment
- When possible, make `match` statements exhaustive and avoid wildcard arms.
- Newly added traits should include doc comments that explain their role and how implementations are expected to use them.
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
@@ -209,3 +210,6 @@ These guidelines apply to app-server protocol work in `codex-rs`, especially:
- Validate with `cargo 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.
+- 修改代码后总是执行 cargo build -p codex-cli; 然后执行交互式终端 (PTY) 验证要完成的特性是否实现;
+- 在跑 codex 命令之前, 总是用sudo codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime 来给二进制签名
+- 当需求改变的时候, 要同步更新对应的文档
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 7a1acc381..87c647b00 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -228,8 +228,6 @@
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/with_cfg.bzl/0.12.0/MODULE.bazel": "b573395fe63aef4299ba095173e2f62ccfee5ad9bbf7acaa95dba73af9fc2b38",
"https://bcr.bazel.build/modules/with_cfg.bzl/0.12.0/source.json": "3f3fbaeafecaf629877ad152a2c9def21f8d330d91aa94c5dc75bbb98c10b8b8",
- "https://bcr.bazel.build/modules/xz/5.4.5.bcr.8/MODULE.bazel": "e48a69bd54053c2ec5fffc2a29fb70122afd3e83ab6c07068f63bc6553fa57cc",
- "https://bcr.bazel.build/modules/xz/5.4.5.bcr.8/source.json": "bd7e928ccd63505b44f4784f7bbf12cc11f9ff23bf3ca12ff2c91cd74846099e",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.8/MODULE.bazel": "772c674bb78a0342b8caf32ab5c25085c493ca4ff08398208dcbe4375fe9f776",
@@ -678,7 +676,6 @@
"bytes_1.11.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"extra-platforms\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.3\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"bytestring_1.5.0": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{\"serde\":[\"dep:serde_core\"]}}",
"bzip2-sys_0.1.13+1.0.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"}],\"features\":{\"__disabled\":[],\"static\":[]}}",
- "bzip2_0.4.4": "{\"dependencies\":[{\"name\":\"bzip2-sys\",\"req\":\"^0.1.11\"},{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"features\":[\"quickcheck\"],\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck6\",\"package\":\"quickcheck\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tokio-core\",\"req\":\"^0.1\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"static\":[\"bzip2-sys/static\"],\"tokio\":[\"tokio-io\",\"futures\"]}}",
"bzip2_0.5.2": "{\"dependencies\":[{\"name\":\"bzip2-sys\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"features\":[\"rust-allocator\",\"semver-prefix\"],\"name\":\"libbz2-rs-sys\",\"optional\":true,\"req\":\"^0.1.3\"},{\"features\":[\"quickcheck1\"],\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5.4\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"dep:bzip2-sys\"],\"libbz2-rs-sys\":[\"dep:libbz2-rs-sys\",\"bzip2-sys?/__disabled\"],\"static\":[\"bzip2-sys?/static\"]}}",
"cached_0.56.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.6\"},{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"cached_proc_macro\",\"optional\":true,\"req\":\"^0.25.0\"},{\"name\":\"cached_proc_macro_types\",\"optional\":true,\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"copy_dir\",\"req\":\"^0.1.3\"},{\"name\":\"directories\",\"optional\":true,\"req\":\"^6.0\"},{\"default_features\":false,\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"googletest\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"r2d2\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"r2d2\"],\"name\":\"redis\",\"optional\":true,\"req\":\"^0.32\"},{\"name\":\"rmp-serde\",\"optional\":true,\"req\":\"^1.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"name\":\"sled\",\"optional\":true,\"req\":\"^0.34\"},{\"kind\":\"dev\",\"name\":\"smartstring\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"macros\",\"time\",\"sync\",\"parking_lot\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"web-time\",\"req\":\"^1.1.0\"}],\"features\":{\"ahash\":[\"dep:ahash\",\"hashbrown/default\"],\"async\":[\"futures\",\"tokio\",\"async-trait\"],\"async_tokio_rt_multi_thread\":[\"async\",\"tokio/rt-multi-thread\"],\"default\":[\"proc_macro\",\"ahash\"],\"disk_store\":[\"sled\",\"serde\",\"rmp-serde\",\"directories\"],\"proc_macro\":[\"cached_proc_macro\",\"cached_proc_macro_types\"],\"redis_ahash\":[\"redis_store\",\"redis/ahash\"],\"redis_async_std\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/async-std-comp\",\"redis/tls\",\"redis/async-std-tls-comp\"],\"redis_connection_manager\":[\"redis_store\",\"redis/connection-manager\"],\"redis_store\":[\"redis\",\"r2d2\",\"serde\",\"serde_json\"],\"redis_tokio\":[\"redis_store\",\"async\",\"redis/aio\",\"redis/tokio-comp\",\"redis/tls\",\"redis/tokio-native-tls-comp\"],\"wasm\":[]}}",
"cached_proc_macro_0.25.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.20.8\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.49\"},{\"name\":\"quote\",\"req\":\"^1.0.6\"},{\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}",
@@ -694,7 +691,6 @@
"cc_1.2.56": "{\"dependencies\":[{\"name\":\"find-msvc-tools\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"jobserver\",\"optional\":true,\"req\":\"^0.1.30\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.62\",\"target\":\"cfg(unix)\"},{\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"jobserver\":[],\"parallel\":[\"dep:libc\",\"dep:jobserver\"]}}",
"cesu8_1.1.0": "{\"dependencies\":[],\"features\":{\"unstable\":[]}}",
"cexpr_0.6.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"clang-sys\",\"req\":\">=0.13.0, <0.29.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"nom\",\"req\":\"^7\"}],\"features\":{}}",
- "cfg-expr_0.20.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.7\"},{\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"name\":\"target-lexicon\",\"optional\":true,\"req\":\"=0.13.3\"}],\"features\":{\"default\":[],\"targets\":[\"target-lexicon\"]}}",
"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\":{}}",
@@ -713,7 +709,6 @@
"clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}",
"cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}",
"cmp_any_0.8.1": "{\"dependencies\":[],\"features\":{}}",
- "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\":[]}}",
"color-spantrace_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ansi-parser\",\"req\":\"^0.8\"},{\"name\":\"once_cell\",\"req\":\"^1.18.0\"},{\"name\":\"owo-colors\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.29\"},{\"name\":\"tracing-core\",\"req\":\"^0.1.21\"},{\"name\":\"tracing-error\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.4\"}],\"features\":{}}",
"color_quant_1.1.0": "{\"dependencies\":[],\"features\":{}}",
@@ -728,7 +723,6 @@
"const-oid_0.9.6": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"}],\"features\":{\"db\":[],\"std\":[]}}",
"const_format_0.2.35": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.7.0\"},{\"name\":\"const_format_proc_macros\",\"req\":\"=0.2.34\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.5\"},{\"default_features\":false,\"name\":\"konst\",\"optional\":true,\"req\":\"^0.2.13\"}],\"features\":{\"__debug\":[\"const_format_proc_macros/debug\"],\"__docsrs\":[],\"__inline_const_pat_tests\":[\"__test\",\"fmt\"],\"__only_new_tests\":[\"__test\"],\"__test\":[],\"all\":[\"fmt\",\"derive\",\"rust_1_64\",\"assert\"],\"assert\":[\"assertc\"],\"assertc\":[\"fmt\",\"assertcp\"],\"assertcp\":[\"rust_1_51\"],\"const_generics\":[\"rust_1_51\"],\"constant_time_as_str\":[\"fmt\"],\"default\":[],\"derive\":[\"fmt\",\"const_format_proc_macros/derive\"],\"fmt\":[\"rust_1_83\"],\"more_str_macros\":[\"rust_1_64\"],\"nightly_const_generics\":[\"const_generics\"],\"rust_1_51\":[],\"rust_1_64\":[\"rust_1_51\",\"konst\",\"konst/rust_1_64\"],\"rust_1_83\":[\"rust_1_64\"]}}",
"const_format_proc_macros_0.2.34": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^1.3.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.19\"},{\"name\":\"quote\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^1.0.38\"},{\"name\":\"unicode-xid\",\"req\":\"^0.2\"}],\"features\":{\"all\":[\"derive\"],\"debug\":[\"syn/extra-traits\"],\"default\":[],\"derive\":[\"syn\",\"syn/derive\",\"syn/printing\"]}}",
- "constant_time_eq_0.1.5": "{\"dependencies\":[],\"features\":{}}",
"constant_time_eq_0.3.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"count_instructions\",\"req\":\"^0.1.3\"},{\"features\":[\"cargo_bench_support\",\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"}],\"features\":{\"count_instructions_test\":[]}}",
"convert_case_0.10.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{}}",
"convert_case_0.6.0": "{\"dependencies\":[{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.18.0\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.9.0\"}],\"features\":{\"random\":[\"rand\"]}}",
@@ -762,11 +756,6 @@
"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\"]}}",
"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\"]}}",
- "cxx_1.0.194": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.101\"},{\"kind\":\"dev\",\"name\":\"cc\",\"req\":\"^1.0.101\"},{\"kind\":\"build\",\"name\":\"cxx-build\",\"req\":\"=1.0.194\",\"target\":\"cfg(any())\"},{\"kind\":\"dev\",\"name\":\"cxx-build\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"cxx-gen\",\"req\":\"=0.7.194\"},{\"kind\":\"dev\",\"name\":\"cxx-test-suite\",\"req\":\"^0\"},{\"kind\":\"build\",\"name\":\"cxxbridge-cmd\",\"req\":\"=1.0.194\",\"target\":\"cfg(any())\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"cxxbridge-flags\",\"req\":\"=1.0.194\"},{\"name\":\"cxxbridge-macro\",\"req\":\"=1.0.194\"},{\"default_features\":false,\"name\":\"foldhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2\"},{\"name\":\"link-cplusplus\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.40\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"kind\":\"dev\",\"name\":\"scratch\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"target-triple\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"alloc\":[],\"c++14\":[\"cxxbridge-flags/c++14\"],\"c++17\":[\"cxxbridge-flags/c++17\"],\"c++20\":[\"cxxbridge-flags/c++20\"],\"default\":[\"std\",\"cxxbridge-flags/default\"],\"std\":[\"alloc\",\"foldhash/std\"]}}",
- "cxxbridge-cmd_1.0.194": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"error-context\",\"help\",\"std\",\"suggestions\",\"usage\"],\"name\":\"clap\",\"req\":\"^4.3.11\"},{\"name\":\"codespan-reporting\",\"req\":\"^0.13.1\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"default_features\":false,\"features\":[\"span-locations\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"full\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{}}",
- "cxxbridge-flags_1.0.194": "{\"dependencies\":[],\"features\":{\"c++14\":[],\"c++17\":[],\"c++20\":[],\"default\":[]}}",
- "cxxbridge-macro_1.0.194": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cxx\",\"req\":\"^1.0\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.35\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{}}",
"darling_0.20.11": "{\"dependencies\":[{\"name\":\"darling_core\",\"req\":\"=0.20.11\"},{\"name\":\"darling_macro\",\"req\":\"=0.20.11\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\",\"target\":\"cfg(compiletests)\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.15\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\",\"target\":\"cfg(compiletests)\"}],\"features\":{\"default\":[\"suggestions\"],\"diagnostics\":[\"darling_core/diagnostics\"],\"suggestions\":[\"darling_core/suggestions\"]}}",
"darling_0.21.3": "{\"dependencies\":[{\"name\":\"darling_core\",\"req\":\"=0.21.3\"},{\"name\":\"darling_macro\",\"req\":\"=0.21.3\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\",\"target\":\"cfg(compiletests)\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.15\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\",\"target\":\"cfg(compiletests)\"}],\"features\":{\"default\":[\"suggestions\"],\"diagnostics\":[\"darling_core/diagnostics\"],\"serde\":[\"darling_core/serde\"],\"suggestions\":[\"darling_core/suggestions\"]}}",
"darling_0.23.0": "{\"dependencies\":[{\"name\":\"darling_core\",\"req\":\"=0.23.0\"},{\"name\":\"darling_macro\",\"req\":\"=0.23.0\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.86\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.18\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\",\"target\":\"cfg(compiletests)\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.15\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.89\",\"target\":\"cfg(compiletests)\"}],\"features\":{\"default\":[\"suggestions\"],\"diagnostics\":[\"darling_core/diagnostics\"],\"serde\":[\"darling_core/serde\"],\"suggestions\":[\"darling_core/suggestions\"]}}",
@@ -785,7 +774,6 @@
"debugid_0.8.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.85\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.37\"},{\"name\":\"uuid\",\"req\":\"^1.0.0\"}],\"features\":{}}",
"debugserver-types_0.5.0": "{\"dependencies\":[{\"name\":\"schemafy\",\"req\":\"^0.5.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}",
"deflate64_0.1.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.13.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.7.1\"}],\"features\":{}}",
- "deno_core_icudata_0.77.0": "{\"dependencies\":[],\"features\":{}}",
"der-parser_10.0.0": "{\"dependencies\":[{\"name\":\"asn1-rs\",\"req\":\"^0.7\"},{\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"cookie-factory\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"nom\",\"req\":\"^7.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.0\"},{\"name\":\"rusticata-macros\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"test-case\",\"req\":\"^3.0\"}],\"features\":{\"as_bitvec\":[\"bitvec\"],\"bigint\":[\"num-bigint\"],\"default\":[\"std\"],\"serialize\":[\"std\",\"cookie-factory\"],\"std\":[],\"unstable\":[]}}",
"der_0.7.10": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9.2\"},{\"name\":\"der_derive\",\"optional\":true,\"req\":\"^0.7.2\"},{\"name\":\"flagset\",\"optional\":true,\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"features\":[\"alloc\"],\"name\":\"pem-rfc7468\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"arbitrary\":[\"dep:arbitrary\",\"const-oid?/arbitrary\",\"std\"],\"bytes\":[\"dep:bytes\",\"alloc\"],\"derive\":[\"dep:der_derive\"],\"oid\":[\"dep:const-oid\"],\"pem\":[\"dep:pem-rfc7468\",\"alloc\",\"zeroize\"],\"real\":[],\"std\":[\"alloc\"]}}",
"deranged_0.5.5": "{\"dependencies\":[{\"name\":\"deranged-macros\",\"optional\":true,\"req\":\"=0.3.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.15\"},{\"default_features\":false,\"name\":\"powerfmt\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.86\"}],\"features\":{\"alloc\":[],\"default\":[],\"macros\":[\"dep:deranged-macros\"],\"num\":[\"dep:num-traits\"],\"powerfmt\":[\"dep:powerfmt\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\"],\"rand09\":[\"dep:rand09\"],\"serde\":[\"dep:serde_core\"]}}",
@@ -822,8 +810,6 @@
"dylint_linting_5.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_cmd\",\"req\":\"^2.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"features\":[\"config\"],\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.23\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"name\":\"toml\",\"req\":\"^0.9\"},{\"kind\":\"build\",\"name\":\"toml\",\"req\":\"^0.9\"}],\"features\":{\"constituent\":[]}}",
"dylint_testing_5.0.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"name\":\"compiletest_rs\",\"req\":\"^0.11\"},{\"name\":\"dylint\",\"req\":\"=5.0.0\"},{\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"regex\",\"req\":\"^1.11\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"tempfile\",\"req\":\"^3.23\"}],\"features\":{\"default\":[],\"deny_warnings\":[]}}",
"dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}",
- "ed25519-dalek_2.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"blake2\",\"req\":\"^0.10\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"digest\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"features\":[\"digest\",\"rand_core\"],\"kind\":\"dev\",\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"ed25519\",\"req\":\">=2.2, <2.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"merlin\",\"optional\":true,\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"signature\",\"optional\":true,\"req\":\">=2.0, <2.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"static_secrets\"],\"kind\":\"dev\",\"name\":\"x25519-dalek\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"ed25519/alloc\",\"serde?/alloc\",\"zeroize/alloc\"],\"asm\":[\"sha2/asm\"],\"batch\":[\"alloc\",\"merlin\",\"rand_core\"],\"default\":[\"fast\",\"std\",\"zeroize\"],\"digest\":[\"signature/digest\"],\"fast\":[\"curve25519-dalek/precomputed-tables\"],\"hazmat\":[],\"legacy_compatibility\":[\"curve25519-dalek/legacy_compatibility\"],\"pem\":[\"alloc\",\"ed25519/pem\",\"pkcs8\"],\"pkcs8\":[\"ed25519/pkcs8\"],\"rand_core\":[\"dep:rand_core\"],\"serde\":[\"dep:serde\",\"ed25519/serde\"],\"std\":[\"alloc\",\"ed25519/std\",\"serde?/std\",\"sha2/std\"],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}",
- "ed25519_2.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"ed25519-dalek\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"signature\"],\"kind\":\"dev\",\"name\":\"ring-compat\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"serde_bytes\",\"optional\":true,\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"signature\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"pkcs8?/alloc\"],\"default\":[\"std\"],\"pem\":[\"alloc\",\"pkcs8/pem\"],\"serde_bytes\":[\"serde\",\"dep:serde_bytes\"],\"std\":[\"pkcs8?/std\",\"signature/std\"]}}",
"either_1.15.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[],\"use_std\":[\"std\"]}}",
"ena_0.14.3": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}",
"encode_unicode_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"features\":[\"https-native\"],\"kind\":\"dev\",\"name\":\"minreq\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
@@ -875,7 +861,6 @@
"foreign-types-shared_0.1.1": "{\"dependencies\":[],\"features\":{}}",
"foreign-types_0.3.2": "{\"dependencies\":[{\"name\":\"foreign-types-shared\",\"req\":\"^0.1\"}],\"features\":{}}",
"form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}",
- "fs2_0.4.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.30\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"},{\"features\":[\"handleapi\",\"processthreadsapi\",\"winerror\",\"fileapi\",\"winbase\",\"std\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
"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\":[]}}",
@@ -900,27 +885,17 @@
"getrandom_0.4.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.77\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), not(any(all(target_os = \\\"linux\\\", target_env = \\\"\\\"), getrandom_backend = \\\"custom\\\", getrandom_backend = \\\"linux_raw\\\", getrandom_backend = \\\"rdrand\\\", getrandom_backend = \\\"rndr\\\"))))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"hurd\\\", target_os = \\\"illumos\\\", target_os = \\\"cygwin\\\", all(target_os = \\\"horizon\\\", target_arch = \\\"arm\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"haiku\\\", target_os = \\\"redox\\\", target_os = \\\"nto\\\", target_os = \\\"aix\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\", target_os = \\\"tvos\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"openbsd\\\", target_os = \\\"vita\\\", target_os = \\\"emscripten\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"netbsd\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"solaris\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.154\",\"target\":\"cfg(target_os = \\\"vxworks\\\")\"},{\"default_features\":false,\"name\":\"r-efi\",\"req\":\"^6\",\"target\":\"cfg(all(target_os = \\\"uefi\\\", getrandom_backend = \\\"efi_rng\\\"))\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"wasip2\",\"req\":\"^1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p2\\\"))\"},{\"name\":\"wasip3\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\", target_env = \\\"p3\\\"))\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"}],\"features\":{\"std\":[],\"sys_rng\":[\"dep:rand_core\"],\"wasm_js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"]}}",
"gif_0.14.1": "{\"dependencies\":[{\"name\":\"color_quant\",\"optional\":true,\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"png\",\"req\":\"^0.18.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.10.0\"},{\"name\":\"weezl\",\"req\":\"^0.1.10\"}],\"features\":{\"color_quant\":[\"dep:color_quant\"],\"default\":[\"raii_no_panic\",\"std\",\"color_quant\"],\"raii_no_panic\":[],\"std\":[]}}",
"gimli_0.32.3": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"fallible-iterator\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"optional\":true,\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"test-assembler\",\"req\":\"^0.1.3\"}],\"features\":{\"default\":[\"read-all\",\"write\"],\"endian-reader\":[\"read\",\"dep:stable_deref_trait\"],\"fallible-iterator\":[\"dep:fallible-iterator\"],\"read\":[\"read-core\"],\"read-all\":[\"read\",\"std\",\"fallible-iterator\",\"endian-reader\"],\"read-core\":[],\"rustc-dep-of-std\":[\"dep:core\",\"dep:alloc\"],\"std\":[\"fallible-iterator?/std\",\"stable_deref_trait?/std\"],\"write\":[\"dep:indexmap\"]}}",
- "gio-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"gobject-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"features\":[\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"v2_58\":[],\"v2_60\":[\"v2_58\"],\"v2_62\":[\"v2_60\"],\"v2_64\":[\"v2_62\"],\"v2_66\":[\"v2_64\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_76\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}",
"git+https://github.com/dzbarsky/rules_rust?rev=b56cbaa8465e74127f1ea216f813cd377295ad81#b56cbaa8465e74127f1ea216f813cd377295ad81_runfiles": "{\"dependencies\":[],\"features\":{},\"strip_prefix\":\"\"}",
"git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"nucleo-matcher\",\"optional\":false},{\"default_features\":true,\"features\":[\"send_guard\",\"arc_lock\"],\"name\":\"parking_lot\",\"optional\":false},{\"name\":\"rayon\"}],\"features\":{},\"strip_prefix\":\"\"}",
"git+https://github.com/helix-editor/nucleo.git?rev=4253de9faabb4e5c6d81d946a5e35a90f87347ee#4253de9faabb4e5c6d81d946a5e35a90f87347ee_nucleo-matcher": "{\"dependencies\":[{\"name\":\"memchr\"},{\"default_features\":true,\"features\":[],\"name\":\"unicode-segmentation\",\"optional\":true}],\"features\":{\"default\":[\"unicode-normalization\",\"unicode-casefold\",\"unicode-segmentation\"],\"unicode-casefold\":[],\"unicode-normalization\":[],\"unicode-segmentation\":[\"dep:unicode-segmentation\"]},\"strip_prefix\":\"matcher\"}",
- "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df171ccb181423f957bb3c1f0#e2d1d1d230c6fc9df171ccb181423f957bb3c1f0_libwebrtc": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"livekit-protocol\",\"optional\":false},{\"name\":\"log\"},{\"features\":[\"derive\"],\"name\":\"serde\"},{\"name\":\"serde_json\"},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"glib\",\"optional\":true,\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"freebsd\\\"))\"},{\"name\":\"cxx\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"lazy_static\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":true,\"features\":[],\"name\":\"livekit-runtime\",\"optional\":false,\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"parking_lot\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rtrb\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"sync\"],\"name\":\"tokio\",\"optional\":false,\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":true,\"features\":[],\"name\":\"webrtc-sys\",\"optional\":false,\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":true,\"features\":[\"MessageEvent\",\"RtcPeerConnection\",\"RtcSignalingState\",\"RtcSdpType\",\"RtcSessionDescriptionInit\",\"RtcPeerConnectionIceEvent\",\"RtcIceCandidate\",\"RtcDataChannel\",\"RtcDataChannelEvent\",\"RtcDataChannelState\",\"EventTarget\",\"WebGlRenderingContext\",\"WebGlTexture\"],\"name\":\"web-sys\",\"optional\":false,\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"jni\",\"target\":\"cfg(target_os = \\\"android\\\")\"}],\"features\":{\"default\":[\"glib-main-loop\"],\"glib-main-loop\":[\"dep:glib\"]},\"strip_prefix\":\"libwebrtc\"}",
- "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df171ccb181423f957bb3c1f0#e2d1d1d230c6fc9df171ccb181423f957bb3c1f0_livekit-protocol": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-util\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"livekit-runtime\",\"optional\":false},{\"name\":\"parking_lot\"},{\"name\":\"pbjson\"},{\"name\":\"pbjson-types\"},{\"name\":\"prost\"},{\"name\":\"serde\"},{\"name\":\"thiserror\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\",\"sync\"],\"name\":\"tokio\",\"optional\":false}],\"features\":{},\"strip_prefix\":\"livekit-protocol\"}",
- "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df171ccb181423f957bb3c1f0#e2d1d1d230c6fc9df171ccb181423f957bb3c1f0_livekit-runtime": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"async-io\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"async-std\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"async-task\",\"optional\":true},{\"name\":\"futures\",\"optional\":true},{\"default_features\":false,\"features\":[\"net\",\"rt\",\"rt-multi-thread\",\"time\"],\"name\":\"tokio\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"tokio-stream\",\"optional\":true}],\"features\":{\"async\":[\"dep:async-std\",\"dep:futures\",\"dep:async-io\"],\"default\":[\"tokio\"],\"dispatcher\":[\"dep:futures\",\"dep:async-io\",\"dep:async-std\",\"dep:async-task\"],\"tokio\":[\"dep:tokio\",\"dep:tokio-stream\"]},\"strip_prefix\":\"livekit-runtime\"}",
- "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df171ccb181423f957bb3c1f0#e2d1d1d230c6fc9df171ccb181423f957bb3c1f0_webrtc-sys": "{\"dependencies\":[{\"name\":\"cxx\"},{\"name\":\"log\"},{\"kind\":\"build\",\"name\":\"cc\"},{\"kind\":\"build\",\"name\":\"cxx-build\"},{\"kind\":\"build\",\"name\":\"glob\"},{\"kind\":\"build\",\"name\":\"pkg-config\"},{\"default_features\":true,\"features\":[],\"kind\":\"build\",\"name\":\"webrtc-sys-build\",\"optional\":false}],\"features\":{\"default\":[]},\"strip_prefix\":\"webrtc-sys\"}",
- "git+https://github.com/juberti-oai/rust-sdks.git?rev=e2d1d1d230c6fc9df171ccb181423f957bb3c1f0#e2d1d1d230c6fc9df171ccb181423f957bb3c1f0_webrtc-sys-build": "{\"dependencies\":[{\"name\":\"anyhow\"},{\"name\":\"fs2\"},{\"name\":\"regex\"},{\"default_features\":false,\"features\":[\"rustls-tls-native-roots\",\"blocking\"],\"name\":\"reqwest\",\"optional\":false},{\"name\":\"scratch\"},{\"name\":\"semver\"},{\"name\":\"zip\"}],\"features\":{},\"strip_prefix\":\"webrtc-sys/build\"}",
- "git+https://github.com/nornagon/crossterm?rev=87db8bfa6dc99427fd3b071681b07fc31c6ce995#87db8bfa6dc99427fd3b071681b07fc31c6ce995_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true},{\"name\":\"parking_lot\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"windows\",\"events\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}",
- "git+https://github.com/nornagon/ratatui?rev=9b2ad1298408c45918ee9f8241a6f95498cdbed2#9b2ad1298408c45918ee9f8241a6f95498cdbed2_ratatui": "{\"dependencies\":[{\"name\":\"bitflags\"},{\"name\":\"cassowary\"},{\"name\":\"compact_str\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"document-features\",\"optional\":true},{\"name\":\"indoc\"},{\"name\":\"instability\"},{\"name\":\"itertools\"},{\"name\":\"lru\"},{\"default_features\":true,\"features\":[],\"name\":\"palette\",\"optional\":true},{\"name\":\"paste\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"strum\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"termwiz\",\"optional\":true},{\"default_features\":true,\"features\":[\"local-offset\"],\"name\":\"time\",\"optional\":true},{\"name\":\"unicode-segmentation\"},{\"name\":\"unicode-truncate\"},{\"name\":\"unicode-width\"},{\"default_features\":true,\"features\":[],\"name\":\"termion\",\"optional\":true,\"target\":\"cfg(not(windows))\"}],\"features\":{\"all-widgets\":[\"widget-calendar\"],\"crossterm\":[\"dep:crossterm\"],\"default\":[\"crossterm\",\"underline-color\"],\"macros\":[],\"palette\":[\"dep:palette\"],\"scrolling-regions\":[],\"serde\":[\"dep:serde\",\"bitflags/serde\",\"compact_str/serde\"],\"termion\":[\"dep:termion\"],\"termwiz\":[\"dep:termwiz\"],\"underline-color\":[\"dep:crossterm\"],\"unstable\":[\"unstable-rendered-line-info\",\"unstable-widget-ref\",\"unstable-backend-writer\"],\"unstable-backend-writer\":[],\"unstable-rendered-line-info\":[],\"unstable-widget-ref\":[],\"widget-calendar\":[\"dep:time\"]},\"strip_prefix\":\"\"}",
+ "git+https://github.com/nornagon/crossterm?branch=nornagon%2Fcolor-query#87db8bfa6dc99427fd3b071681b07fc31c6ce995_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true},{\"name\":\"parking_lot\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"windows\",\"events\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}",
+ "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2_ratatui": "{\"dependencies\":[{\"name\":\"bitflags\"},{\"name\":\"cassowary\"},{\"name\":\"compact_str\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"document-features\",\"optional\":true},{\"name\":\"indoc\"},{\"name\":\"instability\"},{\"name\":\"itertools\"},{\"name\":\"lru\"},{\"default_features\":true,\"features\":[],\"name\":\"palette\",\"optional\":true},{\"name\":\"paste\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"strum\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"termwiz\",\"optional\":true},{\"default_features\":true,\"features\":[\"local-offset\"],\"name\":\"time\",\"optional\":true},{\"name\":\"unicode-segmentation\"},{\"name\":\"unicode-truncate\"},{\"name\":\"unicode-width\"},{\"default_features\":true,\"features\":[],\"name\":\"termion\",\"optional\":true,\"target\":\"cfg(not(windows))\"}],\"features\":{\"all-widgets\":[\"widget-calendar\"],\"crossterm\":[\"dep:crossterm\"],\"default\":[\"crossterm\",\"underline-color\"],\"macros\":[],\"palette\":[\"dep:palette\"],\"scrolling-regions\":[],\"serde\":[\"dep:serde\",\"bitflags/serde\",\"compact_str/serde\"],\"termion\":[\"dep:termion\"],\"termwiz\":[\"dep:termwiz\"],\"underline-color\":[\"dep:crossterm\"],\"unstable\":[\"unstable-rendered-line-info\",\"unstable-widget-ref\",\"unstable-backend-writer\"],\"unstable-backend-writer\":[],\"unstable-rendered-line-info\":[],\"unstable-widget-ref\":[],\"widget-calendar\":[\"dep:time\"]},\"strip_prefix\":\"\"}",
"git+https://github.com/openai-oss-forks/tokio-tungstenite?rev=132f5b39c862e3a970f731d709608b3e6276d5f6#132f5b39c862e3a970f731d709608b3e6276d5f6_tokio-tungstenite": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"optional\":false},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"default_features\":false,\"features\":[],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"tokio-native-tls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tokio-rustls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tungstenite\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\",\"tokio-rustls\",\"stream\",\"tungstenite/__rustls-tls\",\"handshake\"],\"connect\":[\"stream\",\"tokio/net\",\"handshake\"],\"default\":[\"connect\",\"handshake\"],\"handshake\":[\"tungstenite/handshake\"],\"native-tls\":[\"native-tls-crate\",\"tokio-native-tls\",\"stream\",\"tungstenite/native-tls\",\"handshake\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\",\"tungstenite/native-tls-vendored\"],\"proxy\":[\"tungstenite/proxy\",\"tokio/net\",\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"stream\":[],\"url\":[\"tungstenite/url\"]},\"strip_prefix\":\"\"}",
"git+https://github.com/openai-oss-forks/tungstenite-rs?rev=9200079d3b54a1ff51072e24d81fd354f085156f#9200079d3b54a1ff51072e24d81fd354f085156f_tungstenite": "{\"dependencies\":[{\"name\":\"bytes\"},{\"default_features\":true,\"features\":[],\"name\":\"data-encoding\",\"optional\":true},{\"default_features\":false,\"features\":[\"zlib\"],\"name\":\"flate2\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"headers\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"http\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"httparse\",\"optional\":true},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"name\":\"rand\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"sha1\",\"optional\":true},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"url\",\"optional\":true},{\"name\":\"utf-8\"},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"deflate\":[\"headers\",\"flate2\"],\"handshake\":[\"data-encoding\",\"headers\",\"httparse\",\"sha1\"],\"headers\":[\"http\",\"dep:headers\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"proxy\":[\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]},\"strip_prefix\":\"\"}",
"git+https://github.com/rust-lang/rust-clippy?rev=20ce69b9a63bcd2756cd906fe0964d1e901e042a#20ce69b9a63bcd2756cd906fe0964d1e901e042a_clippy_utils": "{\"dependencies\":[{\"default_features\":false,\"features\":[],\"name\":\"arrayvec\",\"optional\":false},{\"name\":\"itertools\"},{\"name\":\"rustc_apfloat\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":false}],\"features\":{},\"strip_prefix\":\"clippy_utils\"}",
"git2_0.20.4": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.13\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"libgit2-sys\",\"req\":\"^0.18.3\"},{\"name\":\"log\",\"req\":\"^0.4.8\"},{\"name\":\"openssl-probe\",\"optional\":true,\"req\":\"^0.1\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.45\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"features\":[\"formatting\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.37\"},{\"name\":\"url\",\"req\":\"^2.5.4\"}],\"features\":{\"default\":[\"ssh\",\"https\"],\"https\":[\"libgit2-sys/https\",\"openssl-sys\",\"openssl-probe\"],\"ssh\":[\"libgit2-sys/ssh\"],\"unstable\":[],\"vendored-libgit2\":[\"libgit2-sys/vendored\"],\"vendored-openssl\":[\"openssl-sys/vendored\",\"libgit2-sys/vendored-openssl\"],\"zlib-ng-compat\":[\"libgit2-sys/zlib-ng-compat\"]}}",
- "glib-macros_0.21.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"glib\",\"req\":\"^0.21\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"proc-macro-crate\",\"req\":\"^3.3\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.104\"},{\"kind\":\"dev\",\"name\":\"trybuild2\",\"req\":\"^1.2\"}],\"features\":{}}",
- "glib-sys_0.21.5": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"v2_58\":[],\"v2_60\":[\"v2_58\"],\"v2_62\":[\"v2_60\"],\"v2_64\":[\"v2_62\"],\"v2_66\":[\"v2_64\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_76\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}",
- "glib_0.21.5": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.9\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"name\":\"futures-channel\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"futures-executor\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"gio-sys\",\"optional\":true,\"req\":\"^0.21\"},{\"kind\":\"dev\",\"name\":\"gir-format-check\",\"req\":\"^0.1\"},{\"name\":\"glib-macros\",\"req\":\"^0.21\"},{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"gobject-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"memchr\",\"req\":\"^2.7.5\"},{\"name\":\"rs-log\",\"optional\":true,\"package\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"union\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"trybuild2\",\"req\":\"^1\"}],\"features\":{\"compiletests\":[],\"default\":[\"gio\"],\"gio\":[\"gio-sys\"],\"log\":[\"rs-log\"],\"log_macros\":[\"log\"],\"v2_58\":[\"glib-sys/v2_58\",\"gobject-sys/v2_58\"],\"v2_60\":[\"v2_58\",\"glib-sys/v2_60\"],\"v2_62\":[\"v2_60\",\"glib-sys/v2_62\",\"gobject-sys/v2_62\"],\"v2_64\":[\"v2_62\",\"glib-sys/v2_64\"],\"v2_66\":[\"v2_64\",\"glib-sys/v2_66\",\"gobject-sys/v2_66\"],\"v2_68\":[\"v2_66\",\"glib-sys/v2_68\",\"gobject-sys/v2_68\"],\"v2_70\":[\"v2_68\",\"glib-sys/v2_70\",\"gobject-sys/v2_70\"],\"v2_72\":[\"v2_70\",\"glib-sys/v2_72\",\"gobject-sys/v2_72\"],\"v2_74\":[\"v2_72\",\"glib-sys/v2_74\",\"gobject-sys/v2_74\"],\"v2_76\":[\"v2_74\",\"glib-sys/v2_76\",\"gobject-sys/v2_76\"],\"v2_78\":[\"v2_76\",\"glib-sys/v2_78\",\"gobject-sys/v2_78\"],\"v2_80\":[\"v2_78\",\"glib-sys/v2_80\",\"gobject-sys/v2_80\"],\"v2_82\":[\"v2_80\",\"glib-sys/v2_82\",\"gobject-sys/v2_82\"],\"v2_84\":[\"v2_82\",\"glib-sys/v2_84\",\"gobject-sys/v2_84\"],\"v2_86\":[\"v2_84\",\"glib-sys/v2_86\",\"gobject-sys/v2_86\"]}}",
"glob_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"}],\"features\":{}}",
"globset_0.4.18": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}",
- "gobject-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"v2_58\":[],\"v2_62\":[\"v2_58\"],\"v2_66\":[\"v2_62\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_74\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}",
"gzip-header_1.0.0": "{\"dependencies\":[{\"name\":\"crc32fast\",\"req\":\"^1.2.1\"}],\"features\":{}}",
"h2_0.4.13": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"stream\":[],\"unstable\":[]}}",
"half_2.7.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.4.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"crunchy\",\"req\":\"^0.2.2\",\"target\":\"cfg(target_arch = \\\"spirv\\\")\"},{\"kind\":\"dev\",\"name\":\"crunchy\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.16\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_distr\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"simd\"],\"name\":\"zerocopy\",\"req\":\"^0.8.26\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"nightly\":[],\"rand_distr\":[\"dep:rand\",\"dep:rand_distr\"],\"std\":[\"alloc\"],\"use-intrinsics\":[],\"zerocopy\":[]}}",
@@ -931,7 +906,6 @@
"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\"]}}",
"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\":[]}}",
- "heck_0.4.1": "{\"dependencies\":[{\"name\":\"unicode-segmentation\",\"optional\":true,\"req\":\"^1.2.0\"}],\"features\":{\"default\":[],\"unicode\":[\"unicode-segmentation\"]}}",
"heck_0.5.0": "{\"dependencies\":[],\"features\":{}}",
"hermit-abi_0.5.2": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[],\"rustc-dep-of-std\":[\"core\",\"alloc\"]}}",
"hex_0.4.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"faster-hex\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rustc-hex\",\"req\":\"^2.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}",
@@ -1004,7 +978,6 @@
"is_ci_1.2.0": "{\"dependencies\":[],\"features\":{}}",
"is_terminal_polyfill_1.70.2": "{\"dependencies\":[],\"features\":{\"default\":[]}}",
"itertools_0.10.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"= 0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}",
- "itertools_0.11.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}",
"itertools_0.12.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}",
"itertools_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}",
"itertools_0.14.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}",
@@ -1026,6 +999,7 @@
"lalrpop_0.19.12": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii-canvas\",\"req\":\"^3.0\"},{\"default_features\":false,\"name\":\"bit-set\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"name\":\"diff\",\"req\":\"^0.1.12\"},{\"default_features\":false,\"name\":\"ena\",\"req\":\"^0.14\"},{\"name\":\"is-terminal\",\"req\":\"^0.4.2\"},{\"default_features\":false,\"features\":[\"use_std\"],\"name\":\"itertools\",\"req\":\"^0.10\"},{\"name\":\"lalrpop-util\",\"req\":\"^0.19.12\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"pico-args\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-case\",\"unicode-perl\"],\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"unicode\"],\"name\":\"regex-syntax\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"unicode-case\",\"unicode-perl\"],\"kind\":\"dev\",\"name\":\"regex-syntax\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"string_cache\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"term\",\"req\":\"^0.7\"},{\"features\":[\"sha3\"],\"name\":\"tiny-keccak\",\"req\":\"^2.0.2\"},{\"default_features\":false,\"name\":\"unicode-xid\",\"req\":\"^0.2\"}],\"features\":{\"default\":[\"lexer\"],\"lexer\":[\"lalrpop-util/lexer\"],\"test\":[]}}",
"landlock_0.4.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"enumflags2\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2.175\"},{\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26\"},{\"kind\":\"dev\",\"name\":\"strum_macros\",\"req\":\"^0.26\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"}],\"features\":{}}",
"language-tags_0.3.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}",
+ "lark-websocket-protobuf_0.1.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.6.0\"},{\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"kind\":\"build\",\"name\":\"prost-build\",\"req\":\"^0.12.6\"}],\"features\":{}}",
"lazy_static_1.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"once\"],\"name\":\"spin\",\"optional\":true,\"req\":\"^0.9.8\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{\"spin_no_std\":[\"spin\"]}}",
"leb128fmt_0.1.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[]}}",
"libc_0.2.182": "{\"dependencies\":[{\"name\":\"rustc-std-workspace-core\",\"optional\":true,\"req\":\"^1.0.1\"}],\"features\":{\"align\":[],\"const-extern-fn\":[],\"default\":[\"std\"],\"extra_traits\":[],\"rustc-dep-of-std\":[\"align\",\"rustc-std-workspace-core\"],\"std\":[],\"use_std\":[\"std\"]}}",
@@ -1040,7 +1014,6 @@
"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\"]}}",
- "link-cplusplus_1.0.12": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1\"}],\"features\":{\"default\":[],\"libc++\":[],\"libcxx\":[\"libc++\"],\"libstdc++\":[],\"libstdcxx\":[\"libstdc++\"],\"nothing\":[]}}",
"linked-hash-map_0.5.6": "{\"dependencies\":[{\"name\":\"heapsize\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"heapsize_impl\":[\"heapsize\"],\"nightly\":[],\"serde_impl\":[\"serde\"]}}",
"linux-keyutils_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\",\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4.11\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.132\"},{\"kind\":\"dev\",\"name\":\"zeroize\",\"req\":\"^1.5.7\"}],\"features\":{\"default\":[],\"std\":[\"bitflags/std\"]}}",
"linux-raw-sys_0.11.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.100\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"auxvec\":[],\"bootparam\":[],\"btrfs\":[],\"default\":[\"std\",\"general\",\"errno\"],\"elf\":[],\"elf_uapi\":[],\"errno\":[],\"general\":[],\"if_arp\":[],\"if_ether\":[],\"if_packet\":[],\"image\":[],\"io_uring\":[],\"ioctl\":[],\"landlock\":[],\"loop_device\":[],\"mempolicy\":[],\"net\":[],\"netlink\":[],\"no_std\":[],\"prctl\":[],\"ptrace\":[],\"rustc-dep-of-std\":[\"core\",\"no_std\"],\"std\":[],\"system\":[],\"xdp\":[]}}",
@@ -1134,6 +1107,26 @@
"onig_6.5.1": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(windows)\"},{\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"onig_sys\",\"req\":\"^69.9.1\"}],\"features\":{\"default\":[\"generate\"],\"generate\":[\"onig_sys/generate\"],\"posix-api\":[\"onig_sys/posix-api\"],\"print-debug\":[\"onig_sys/print-debug\"],\"std-pattern\":[]}}",
"onig_sys_69.9.1": "{\"dependencies\":[{\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.71\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.16\"}],\"features\":{\"default\":[\"generate\"],\"generate\":[\"bindgen\"],\"posix-api\":[],\"print-debug\":[]}}",
"opaque-debug_0.3.1": "{\"dependencies\":[],\"features\":{}}",
+ "openlark-ai_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"}],\"features\":{\"default\":[\"v1\"],\"full\":[\"v1\"],\"v1\":[]}}",
+ "openlark-analytics_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"name\":\"futures\",\"req\":\"^0.3.30\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"all-analytics\":[\"full\"],\"analytics\":[\"search\",\"report\"],\"core\":[],\"default\":[\"search\",\"report\"],\"full\":[\"search\",\"report\",\"v4\"],\"report\":[\"report-core\",\"core\"],\"report-core\":[],\"search\":[\"search-core\",\"core\"],\"search-core\":[],\"v1\":[\"core\"],\"v2\":[\"v1\"],\"v3\":[\"v2\"],\"v4\":[\"v3\"]}}",
+ "openlark-application_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"async\":[\"tokio\"],\"default\":[\"v1\",\"async\"],\"full\":[\"v1\",\"async\"],\"v1\":[]}}",
+ "openlark-auth_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"features\":[\"serde\",\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"features\":[\"html_reports\"],\"name\":\"criterion\",\"optional\":true,\"req\":\"^0.5\"},{\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12.7\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.18\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"full\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"serde\"],\"name\":\"url\",\"optional\":true,\"req\":\"^2.5.0\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"v4\",\"serde\",\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"advanced-cache\":[\"cache\",\"encryption\"],\"cache\":[\"token-management\"],\"default\":[\"token-management\",\"cache\",\"oauth\"],\"encryption\":[\"ring\",\"sha2\",\"hmac\",\"pbkdf2\"],\"monitoring\":[],\"oauth\":[\"reqwest\",\"url\"],\"token-management\":[]}}",
+ "openlark-cardkit_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"kind\":\"dev\",\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"default\":[\"v1\"],\"full\":[\"v1\"],\"v1\":[]}}",
+ "openlark-client_0.15.0-rc.2": "{\"dependencies\":[{\"features\":[\"serde\",\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.30\"},{\"name\":\"lark-websocket-protobuf\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"openlark-ai\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-auth\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-cardkit\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-communication\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-docs\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-hr\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-meeting\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"openlark-security\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"prost\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"rustls-tls-native-roots\"],\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.23\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"serde\"],\"name\":\"url\",\"optional\":true,\"req\":\"^2.5.0\"},{\"features\":[\"v4\",\"serde\",\"v4\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"ai\":[\"dep:openlark-ai\"],\"auth\":[\"dep:openlark-auth\"],\"cardkit\":[\"auth\",\"dep:openlark-cardkit\"],\"communication\":[\"auth\",\"dep:openlark-communication\"],\"core-layer\":[\"communication\",\"docs\",\"security\"],\"default\":[\"auth\",\"communication\"],\"docs\":[\"auth\",\"dep:openlark-docs\"],\"hr\":[\"dep:openlark-hr\"],\"meeting\":[\"auth\",\"dep:openlark-meeting\"],\"p0-services\":[\"communication\",\"docs\",\"security\"],\"security\":[\"auth\",\"dep:openlark-security\"],\"websocket\":[\"tokio-tungstenite\",\"futures-util\",\"lark-websocket-protobuf\",\"url\",\"prost\",\"reqwest\",\"log\"]}}",
+ "openlark-communication_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"aily\":[],\"contact\":[],\"default\":[\"im\",\"contact\",\"moments\"],\"full\":[\"im\",\"contact\",\"moments\",\"aily\"],\"im\":[],\"moments\":[]}}",
+ "openlark-core_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"req\":\"^0.3.30\"},{\"name\":\"hmac\",\"req\":\"^0.12.1\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"lark-websocket-protobuf\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"num_cpus\",\"req\":\"^1.16\"},{\"name\":\"openlark-protocol\",\"optional\":true,\"req\":\"^0.15.0-rc.2\"},{\"name\":\"opentelemetry\",\"optional\":true,\"req\":\"^0.24\"},{\"name\":\"opentelemetry-otlp\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"rt-tokio\"],\"name\":\"opentelemetry_sdk\",\"optional\":true,\"req\":\"^0.24\"},{\"name\":\"prost\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"quick_cache\",\"req\":\"^0.6.3\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_with\",\"req\":\"^3\"},{\"name\":\"sha2\",\"req\":\"^0.10.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"features\":[\"rustls-tls-native-roots\"],\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.23\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"tracing-opentelemetry\",\"optional\":true,\"req\":\"^0.25\"},{\"features\":[\"env-filter\",\"json\"],\"name\":\"tracing-subscriber\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"env-filter\",\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"default\":[\"testing\"],\"otel\":[\"tracing-init\",\"opentelemetry\",\"opentelemetry_sdk\",\"opentelemetry-otlp\",\"tracing-opentelemetry\"],\"testing\":[\"tracing-init\"],\"tracing-init\":[\"tracing-subscriber\"],\"websocket\":[\"tokio-tungstenite\",\"prost\",\"openlark-protocol\",\"lark-websocket-protobuf\"]}}",
+ "openlark-docs_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"name\":\"futures\",\"req\":\"^0.3.30\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"all-cloud-docs\":[\"full\"],\"baike\":[],\"base\":[\"core\"],\"bitable\":[\"core\"],\"ccm\":[\"ccm-core\",\"ccm-doc\",\"ccm-docx\",\"ccm-drive\",\"ccm-sheets\",\"ccm-wiki\"],\"ccm-core\":[],\"ccm-doc\":[\"ccm-core\"],\"ccm-docx\":[\"ccm-core\"],\"ccm-drive\":[\"ccm-core\"],\"ccm-sheets\":[\"ccm-sheets-v3\"],\"ccm-sheets-v3\":[\"ccm-core\"],\"ccm-wiki\":[\"ccm-core\"],\"cloud-docs\":[\"ccm\",\"bitable\",\"base\"],\"core\":[],\"default\":[],\"docs\":[\"ccm-doc\"],\"docx\":[\"ccm-docx\"],\"full\":[\"ccm\",\"bitable\",\"base\",\"baike\",\"minutes\",\"v3\"],\"lingo\":[],\"minutes\":[\"core\"],\"v1\":[\"core\"],\"v2\":[\"v1\"],\"v3\":[\"v2\"],\"wiki\":[\"ccm-wiki\"]}}",
+ "openlark-helpdesk_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"async\":[\"tokio\"],\"default\":[\"v1\",\"async\"],\"full\":[\"v1\",\"async\"],\"v1\":[]}}",
+ "openlark-hr_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"attendance\":[],\"compensation\":[],\"corehr\":[],\"default\":[\"attendance\",\"corehr\",\"compensation\",\"payroll\",\"performance\",\"okr\",\"hire\",\"ehr\"],\"ehr\":[],\"hire\":[],\"hr-full\":[\"attendance\",\"corehr\",\"compensation\",\"payroll\",\"performance\",\"okr\",\"hire\",\"ehr\"],\"okr\":[],\"payroll\":[],\"performance\":[]}}",
+ "openlark-mail_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"async\":[\"tokio\"],\"default\":[\"v1\",\"async\"],\"full\":[\"v1\",\"async\"],\"v1\":[]}}",
+ "openlark-meeting_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1\"}],\"features\":{\"calendar\":[\"calendar-v4\"],\"calendar-v4\":[],\"default\":[\"vc\",\"calendar\"],\"full\":[\"vc\",\"calendar\",\"meeting-room\"],\"meeting-room\":[\"meeting-room-v1\"],\"meeting-room-v1\":[],\"vc\":[\"vc-v1\"],\"vc-v1\":[]}}",
+ "openlark-platform_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"name\":\"futures\",\"req\":\"^0.3.30\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"admin\":[\"admin-core\",\"core\"],\"admin-core\":[],\"all-platform\":[\"full\"],\"app-engine\":[\"app-engine-core\",\"core\"],\"app-engine-core\":[],\"core\":[],\"default\":[\"app-engine\",\"directory\",\"admin\",\"mdm\",\"tenant\",\"trust_party\"],\"directory\":[\"directory-core\",\"core\"],\"directory-core\":[],\"full\":[\"app-engine\",\"directory\",\"admin\",\"mdm\",\"tenant\",\"trust_party\",\"v4\"],\"mdm\":[\"mdm-core\",\"core\"],\"mdm-core\":[],\"platform\":[\"app-engine\",\"directory\",\"admin\"],\"tenant\":[\"tenant-core\",\"core\"],\"tenant-core\":[],\"trust_party\":[\"trust_party-core\",\"core\"],\"trust_party-core\":[],\"v1\":[\"core\"],\"v2\":[\"v1\"],\"v3\":[\"v2\"],\"v4\":[\"v3\"]}}",
+ "openlark-protocol_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.6.0\"},{\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"kind\":\"build\",\"name\":\"prost-build\",\"req\":\"^0.12.6\"}],\"features\":{}}",
+ "openlark-security_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"name\":\"hmac\",\"req\":\"^0.12.1\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"name\":\"sha2\",\"req\":\"^0.10.8\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"acs\":[\"auth\"],\"audit\":[\"core\"],\"auth\":[\"core\"],\"compliance\":[\"auth\"],\"core\":[],\"default\":[\"auth\",\"acs\"],\"full\":[\"auth\",\"acs\",\"audit\",\"token\",\"compliance\",\"v3\"],\"security\":[\"full\"],\"token\":[\"auth\"],\"v1\":[\"core\"],\"v2\":[\"v1\"],\"v3\":[\"v2\"]}}",
+ "openlark-user_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.86\"},{\"name\":\"async-trait\",\"req\":\"^0.1.83\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"name\":\"futures\",\"req\":\"^0.3.30\"},{\"name\":\"log\",\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"name\":\"url\",\"req\":\"^2.5.0\"},{\"features\":[\"v4\",\"serde\"],\"name\":\"uuid\",\"req\":\"^1.6\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"all-user\":[\"full\"],\"core\":[],\"default\":[\"settings\",\"preferences\"],\"full\":[\"settings\",\"preferences\",\"v4\"],\"preferences\":[\"preferences-core\",\"core\"],\"preferences-core\":[],\"settings\":[\"settings-core\",\"core\"],\"settings-core\":[],\"user\":[\"settings\",\"preferences\"],\"v1\":[\"core\"],\"v2\":[\"v1\"],\"v3\":[\"v2\"],\"v4\":[\"v3\"]}}",
+ "openlark-webhook_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12.1\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.8\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"card\":[],\"default\":[\"robot\"],\"robot\":[],\"signature\":[\"hmac\",\"sha2\",\"base64\"]}}",
+ "openlark-workflow_0.15.0-rc.2": "{\"dependencies\":[{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.2\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"name\":\"tracing\",\"req\":\"^0.1\"}],\"features\":{\"async\":[\"tokio\"],\"board\":[],\"default\":[\"v1\",\"v2\",\"async\",\"board\"],\"full\":[\"v1\",\"v2\",\"async\",\"board\"],\"v1\":[],\"v2\":[]}}",
+ "openlark_0.15.0-rc.1": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"colored\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"dotenvy\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.19\"},{\"name\":\"openlark-ai\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-analytics\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-application\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-auth\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-cardkit\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-client\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-communication\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-core\",\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-docs\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-helpdesk\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-hr\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-mail\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-meeting\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-platform\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-protocol\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-security\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-user\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-webhook\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"name\":\"openlark-workflow\",\"optional\":true,\"req\":\"^0.15.0-rc.1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"multipart\",\"rustls-tls\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12.7\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.19\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_repr\",\"req\":\"^0.1.19\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.8\"},{\"kind\":\"dev\",\"name\":\"test-log\",\"req\":\"^0.2\"},{\"features\":[\"rt\",\"rt-multi-thread\",\"macros\",\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38\"},{\"kind\":\"dev\",\"name\":\"tracing-test\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{\"ai\":[\"client\",\"openlark-ai\"],\"analytics\":[\"client\",\"openlark-analytics\"],\"application\":[\"client\",\"openlark-application\"],\"auth\":[\"client\",\"openlark-auth\"],\"base\":[\"client\",\"openlark-docs\"],\"bitable\":[\"client\",\"openlark-docs\"],\"cardkit\":[\"client\",\"openlark-cardkit\"],\"client\":[\"openlark-client\"],\"communication\":[\"client\",\"openlark-communication\"],\"core-services\":[\"auth\",\"communication\",\"docs\",\"workflow\"],\"default\":[\"core-services\"],\"dev-tools\":[],\"docs\":[\"client\",\"openlark-docs\"],\"helpdesk\":[\"client\",\"openlark-helpdesk\"],\"hr\":[\"client\",\"openlark-hr\"],\"mail\":[\"client\",\"openlark-mail\"],\"meeting\":[\"client\",\"openlark-meeting\"],\"platform\":[\"client\",\"openlark-platform\"],\"protocol\":[\"openlark-protocol\"],\"security\":[\"client\",\"openlark-security\"],\"user\":[\"client\",\"openlark-user\"],\"webhook\":[\"client\",\"openlark-webhook\"],\"websocket\":[\"protocol\",\"openlark-client/websocket\"],\"workflow\":[\"client\",\"openlark-workflow\"]}}",
"openssl-macros_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
"openssl-probe_0.1.6": "{\"dependencies\":[],\"features\":{}}",
"openssl-probe_0.2.1": "{\"dependencies\":[],\"features\":{}}",
@@ -1156,16 +1149,11 @@
"parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}",
"parking_lot_0.12.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"lock_api\",\"req\":\"^0.4.14\"},{\"name\":\"parking_lot_core\",\"req\":\"^0.9.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"}],\"features\":{\"arc_lock\":[\"lock_api/arc_lock\"],\"deadlock_detection\":[\"parking_lot_core/deadlock_detection\"],\"default\":[],\"hardware-lock-elision\":[],\"nightly\":[\"parking_lot_core/nightly\",\"lock_api/nightly\"],\"owning_ref\":[\"lock_api/owning_ref\"],\"send_guard\":[],\"serde\":[\"lock_api/serde\"]}}",
"parking_lot_core_0.9.12": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.60\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.95\",\"target\":\"cfg(unix)\"},{\"name\":\"petgraph\",\"optional\":true,\"req\":\"^0.6.0\"},{\"name\":\"redox_syscall\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"deadlock_detection\":[\"petgraph\",\"backtrace\"],\"nightly\":[]}}",
- "password-hash_0.4.2": "{\"dependencies\":[{\"name\":\"base64ct\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{\"alloc\":[\"base64ct/alloc\"],\"default\":[\"rand_core\"],\"std\":[\"alloc\",\"base64ct/std\",\"rand_core/std\"]}}",
"paste_1.0.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste-test-suite\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}",
"pastey_0.2.1": "{\"dependencies\":[],\"features\":{}}",
"path-absolutize_3.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"path-dedot\",\"req\":\"^3.1.1\"},{\"kind\":\"dev\",\"name\":\"slash-formatter\",\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"lazy_static_cache\":[\"path-dedot/lazy_static_cache\"],\"once_cell_cache\":[\"path-dedot/once_cell_cache\"],\"unsafe_cache\":[\"path-dedot/unsafe_cache\"],\"use_unix_paths_on_wasm\":[\"path-dedot/use_unix_paths_on_wasm\"]}}",
"path-dedot_3.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"lazy_static\",\"optional\":true,\"req\":\"^1.4\"},{\"name\":\"once_cell\",\"req\":\"^1.4\"}],\"features\":{\"lazy_static_cache\":[\"lazy_static\"],\"once_cell_cache\":[],\"unsafe_cache\":[],\"use_unix_paths_on_wasm\":[]}}",
"pathdiff_0.2.3": "{\"dependencies\":[{\"name\":\"camino\",\"optional\":true,\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1.0.0\"}],\"features\":{}}",
- "pbjson-build_0.6.2": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.4\"},{\"name\":\"itertools\",\"req\":\"^0.11\"},{\"name\":\"prost\",\"req\":\"^0.12\"},{\"name\":\"prost-types\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1\"}],\"features\":{}}",
- "pbjson-types_0.6.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"pbjson\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"pbjson-build\",\"req\":\"^0.6\"},{\"name\":\"prost\",\"req\":\"^0.12\"},{\"kind\":\"build\",\"name\":\"prost-build\",\"req\":\"^0.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{}}",
- "pbjson_0.6.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.21\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"}],\"features\":{}}",
- "pbkdf2_0.11.0": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"hmac\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"sha1\",\"optional\":true,\"package\":\"sha-1\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"package\":\"sha-1\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\"],\"parallel\":[\"rayon\",\"std\"],\"simple\":[\"hmac\",\"password-hash\",\"sha2\"],\"std\":[\"password-hash/std\"]}}",
"pbkdf2_0.12.2": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"hmac\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.7\"},{\"default_features\":false,\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"hmac\"],\"parallel\":[\"rayon\",\"std\"],\"simple\":[\"hmac\",\"password-hash\",\"sha2\"],\"std\":[\"password-hash/std\"]}}",
"pem-rfc7468_0.7.0": "{\"dependencies\":[{\"name\":\"base64ct\",\"req\":\"^1.4\"}],\"features\":{\"alloc\":[\"base64ct/alloc\"],\"std\":[\"alloc\",\"base64ct/std\"]}}",
"pem_3.0.6": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde_core\"],\"std\":[\"base64/std\",\"serde_core?/std\"]}}",
@@ -1208,10 +1196,12 @@
"prost-build_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"name\":\"once_cell\",\"req\":\"^1.17.1\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.6\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.9.1\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^10.0.1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}",
"prost-build_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.14\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"petgraph\",\"req\":\"^0.8\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^22\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}",
"prost-derive_0.12.6": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
+ "prost-derive_0.13.5": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.14\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
"prost-derive_0.14.3": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.14\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
"prost-types_0.12.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"prost-derive\"],\"name\":\"prost\",\"req\":\"^0.12.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"prost/std\"]}}",
"prost-types_0.14.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4\"},{\"default_features\":false,\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"prost\",\"req\":\"^0.14.3\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\"],\"std\":[\"prost/std\"]}}",
"prost_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.12.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"prost-derive\":[\"derive\"],\"std\":[]}}",
+ "prost_0.13.5": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.13.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"prost-derive\":[\"derive\"],\"std\":[]}}",
"prost_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"std\":[]}}",
"psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}",
"psl_2.1.184": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}",
@@ -1220,6 +1210,7 @@
"pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}",
"quick-error_2.0.1": "{\"dependencies\":[],\"features\":{}}",
"quick-xml_0.38.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.8\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.139\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}",
+ "quick_cache_0.6.21": "{\"dependencies\":[{\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"crossbeam-utils\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"equivalent\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.16\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_distr\",\"req\":\"^0.5\"},{\"name\":\"shuttle\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"}],\"features\":{\"default\":[\"ahash\",\"parking_lot\"],\"sharded-lock\":[\"dep:crossbeam-utils\"],\"shuttle\":[\"dep:shuttle\"],\"stats\":[]}}",
"quinn-proto_0.11.13": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}",
"quinn-udp_0.5.14": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"libc\",\"req\":\"^0.2.158\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.19\",\"target\":\"cfg(windows)\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"log\"],\"direct-log\":[\"dep:log\"],\"fast-apple-datapath\":[],\"log\":[\"tracing/log\"]}}",
"quinn_0.11.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.22\"},{\"name\":\"async-io\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.11\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"crc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"directories-next\",\"req\":\"^2\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.19\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"proto\",\"package\":\"quinn-proto\",\"req\":\"^0.11.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"smol\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"time\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"std-future\"],\"kind\":\"dev\",\"name\":\"tracing-futures\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"features\":[\"tracing\"],\"name\":\"udp\",\"package\":\"quinn-udp\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"aws-lc-rs\":[\"proto/aws-lc-rs\"],\"aws-lc-rs-fips\":[\"proto/aws-lc-rs-fips\"],\"bloom\":[\"proto/bloom\"],\"default\":[\"log\",\"platform-verifier\",\"runtime-tokio\",\"rustls-ring\",\"bloom\"],\"lock_tracking\":[],\"log\":[\"tracing/log\",\"proto/log\",\"udp/log\"],\"platform-verifier\":[\"proto/platform-verifier\"],\"qlog\":[\"proto/qlog\"],\"ring\":[\"proto/ring\"],\"runtime-async-std\":[\"async-io\",\"async-std\"],\"runtime-smol\":[\"async-io\",\"smol\"],\"runtime-tokio\":[\"tokio/time\",\"tokio/rt\",\"tokio/net\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"aws-lc-rs\",\"proto/rustls-aws-lc-rs\",\"proto/aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"dep:rustls\",\"aws-lc-rs-fips\",\"proto/rustls-aws-lc-rs-fips\",\"proto/aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"ring\",\"proto/rustls-ring\",\"proto/ring\"]}}",
@@ -1246,7 +1237,7 @@
"rama-unix_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"}],\"features\":{\"default\":[]}}",
"rama-utils_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"regex\",\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"},{\"features\":[\"write\",\"serde\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"name\":\"smol_str\",\"req\":\"^0.3\"},{\"features\":[\"time\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wildcard\",\"req\":\"^0.3\"}],\"features\":{}}",
"rand_0.8.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.22\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"features\":[\"into_bits\"],\"name\":\"packed_simd\",\"optional\":true,\"package\":\"packed_simd_2\",\"req\":\"^0.3.7\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"}],\"features\":{\"alloc\":[\"rand_core/alloc\"],\"default\":[\"std\",\"std_rng\"],\"getrandom\":[\"rand_core/getrandom\"],\"min_const_gen\":[],\"nightly\":[],\"serde1\":[\"serde\",\"rand_core/serde1\"],\"simd_support\":[\"packed_simd\"],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha/std\",\"alloc\",\"getrandom\",\"libc\"],\"std_rng\":[\"rand_chacha\"]}}",
- "rand_0.9.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_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"std_rng\",\"os_rng\",\"small_rng\",\"thread_rng\"],\"log\":[\"dep:log\"],\"nightly\":[],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\",\"rand_core/serde\"],\"simd_support\":[],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha?/std\",\"alloc\"],\"std_rng\":[\"dep:rand_chacha\"],\"thread_rng\":[\"std\",\"std_rng\",\"os_rng\"],\"unbiased\":[]}}",
"rand_chacha_0.3.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.8\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde1\":[\"serde\"],\"simd\":[],\"std\":[\"ppv-lite86/std\"]}}",
"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.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\"]}}",
@@ -1277,7 +1268,6 @@
"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\"]}}",
- "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\"]}}",
"rust-embed_8.11.0": "{\"dependencies\":[{\"name\":\"actix-web\",\"optional\":true,\"req\":\"^4\"},{\"default_features\":false,\"features\":[\"http1\",\"tokio\"],\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"hex\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"include-flate\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.5\"},{\"default_features\":false,\"features\":[\"server\"],\"name\":\"poem\",\"optional\":true,\"req\":\"^1.3.30\"},{\"default_features\":false,\"name\":\"rocket\",\"optional\":true,\"req\":\"^0.5.0-rc.2\"},{\"name\":\"rust-embed-impl\",\"req\":\"^8.9.0\"},{\"name\":\"rust-embed-utils\",\"req\":\"^8.9.0\"},{\"default_features\":false,\"name\":\"salvo\",\"optional\":true,\"req\":\"^0.16\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"default_features\":false,\"name\":\"warp\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"actix\":[\"actix-web\",\"mime_guess\"],\"axum-ex\":[\"axum\",\"tokio\",\"mime_guess\"],\"compression\":[\"rust-embed-impl/compression\",\"include-flate\"],\"debug-embed\":[\"rust-embed-impl/debug-embed\",\"rust-embed-utils/debug-embed\"],\"deterministic-timestamps\":[\"rust-embed-impl/deterministic-timestamps\"],\"include-exclude\":[\"rust-embed-impl/include-exclude\",\"rust-embed-utils/include-exclude\"],\"interpolate-folder-path\":[\"rust-embed-impl/interpolate-folder-path\"],\"mime-guess\":[\"rust-embed-impl/mime-guess\",\"rust-embed-utils/mime-guess\"],\"poem-ex\":[\"poem\",\"tokio\",\"mime_guess\",\"hex\"],\"salvo-ex\":[\"salvo\",\"tokio\",\"mime_guess\",\"hex\"],\"warp-ex\":[\"warp\",\"tokio\",\"mime_guess\"]}}",
@@ -1292,9 +1282,11 @@
"rustix_0.38.44": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.49\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.161\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.161\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", target_arch = \\\"s390x\\\"), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.5.2\",\"target\":\"cfg(any(target_os = \\\"android\\\", target_os = \\\"linux\\\"))\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_NetworkManagement_IpHelper\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"procfs\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"cc\":[],\"default\":[\"std\",\"use-libc-auxv\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"linux-raw-sys/io_uring\"],\"libc-extra-traits\":[\"libc?/extra_traits\"],\"linux_4_11\":[],\"linux_latest\":[\"linux_4_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[\"fs\"],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"procfs\":[\"once_cell\",\"itoa\",\"fs\"],\"pty\":[\"itoa\",\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"compiler_builtins\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\",\"compiler_builtins?/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\",\"libc-extra-traits\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\",\"libc-extra-traits\"],\"use-libc-auxv\":[]}}",
"rustix_1.1.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}",
"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.7.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.1.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^2\",\"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\":\"^0.26\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.16\"}],\"features\":{}}",
"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-pemfile_2.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.9\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"pki-types/std\"]}}",
"rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}",
- "rustls-webpki_0.103.12": "{\"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-webpki_0.103.10": "{\"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\":{}}",
"rustyline_14.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.2\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"default_features\":false,\"name\":\"buffer-redux\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"clipboard-win\",\"req\":\"^5.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"fd-lock\",\"optional\":true,\"req\":\"^4.0.0\"},{\"name\":\"home\",\"optional\":true,\"req\":\"^0.5.4\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"memchr\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"fs\",\"ioctl\",\"poll\",\"signal\",\"term\"],\"name\":\"nix\",\"req\":\"^0.28\",\"target\":\"cfg(unix)\"},{\"name\":\"radix_trie\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"bundled\",\"backup\"],\"name\":\"rusqlite\",\"optional\":true,\"req\":\"^0.31.0\"},{\"name\":\"rustyline-derive\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"signal-hook\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"skim\",\"optional\":true,\"req\":\"^0.10\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"name\":\"termios\",\"optional\":true,\"req\":\"^0.3.3\",\"target\":\"cfg(unix)\"},{\"name\":\"unicode-segmentation\",\"req\":\"^1.0\"},{\"name\":\"unicode-width\",\"req\":\"^0.1\"},{\"name\":\"utf8parse\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Security\",\"Win32_System_Threading\",\"Win32_UI_Input_KeyboardAndMouse\"],\"name\":\"windows-sys\",\"req\":\"^0.52.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"case_insensitive_history_search\":[\"regex\"],\"custom-bindings\":[\"radix_trie\"],\"default\":[\"custom-bindings\",\"with-dirs\",\"with-file-history\"],\"derive\":[\"rustyline-derive\"],\"with-dirs\":[\"home\"],\"with-file-history\":[\"fd-lock\"],\"with-fuzzy\":[\"skim\"],\"with-sqlite-history\":[\"rusqlite\"]}}",
@@ -1313,7 +1305,6 @@
"schemars_derive_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.2.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"serde_derive_internals\",\"req\":\"^0.29.1\"},{\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"extra-traits\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
"scoped-tls_1.0.1": "{\"dependencies\":[],\"features\":{}}",
"scopeguard_1.2.0": "{\"dependencies\":[],\"features\":{\"default\":[\"use_std\"],\"use_std\":[]}}",
- "scratch_1.0.9": "{\"dependencies\":[],\"features\":{}}",
"scrypt_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"password-hash\",\"req\":\"^0.5\"},{\"name\":\"pbkdf2\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"salsa20\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\",\"std\"],\"simple\":[\"password-hash\"],\"std\":[\"password-hash/std\"]}}",
"sdd_3.0.10": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{}}",
"seccompiler_0.5.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.153\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.27\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.9\"}],\"features\":{\"json\":[\"serde\",\"serde_json\"]}}",
@@ -1412,10 +1403,8 @@
"sys-locale_0.3.2": "{\"dependencies\":[{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"},{\"features\":[\"Window\",\"WorkerGlobalScope\",\"Navigator\",\"WorkerNavigator\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(unix)))\"}],\"features\":{\"js\":[\"js-sys\",\"wasm-bindgen\",\"web-sys\"]}}",
"system-configuration-sys_0.6.0": "{\"dependencies\":[{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.149\"}],\"features\":{}}",
"system-configuration_0.7.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"core-foundation\",\"req\":\"^0.9\"},{\"name\":\"system-configuration-sys\",\"req\":\"^0.6\"}],\"features\":{}}",
- "system-deps_7.0.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.5\"},{\"features\":[\"targets\"],\"name\":\"cfg-expr\",\"req\":\">=0.17, <0.21\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"pkg-config\",\"req\":\"^0.3.25\"},{\"default_features\":false,\"features\":[\"parse\",\"std\"],\"name\":\"toml\",\"req\":\"^0.9\"},{\"name\":\"version-compare\",\"req\":\"^0.2\"}],\"features\":{}}",
"tagptr_0.2.0": "{\"dependencies\":[],\"features\":{}}",
"tar_0.4.44": "{\"dependencies\":[{\"name\":\"filetime\",\"req\":\"^0.2.8\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"xattr\",\"optional\":true,\"req\":\"^1.1.3\",\"target\":\"cfg(unix)\"}],\"features\":{\"default\":[\"xattr\"]}}",
- "target-lexicon_0.13.3": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"arch_z80\":[],\"arch_zkasm\":[],\"default\":[],\"serde_support\":[\"serde\",\"std\"],\"std\":[]}}",
"tempfile_3.24.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.3\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}",
"tempfile_3.27.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"fastrand\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"getrandom\",\"optional\":true,\"req\":\">=0.3.0, <0.5\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"once_cell\",\"req\":\"^1.19.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.1.4\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Storage_FileSystem\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"getrandom\"],\"nightly\":[]}}",
"temporal_capi_0.1.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"diplomat\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"name\":\"diplomat-runtime\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"features\":[\"unstable\"],\"name\":\"icu_calendar\",\"req\":\"^2.1.0\"},{\"name\":\"icu_locale\",\"req\":\"^2.1.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.19\"},{\"default_features\":false,\"name\":\"temporal_rs\",\"req\":\"^0.1.2\"},{\"name\":\"timezone_provider\",\"req\":\"^0.1.2\"},{\"name\":\"writeable\",\"req\":\"^0.6.0\"},{\"name\":\"zoneinfo64\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"compiled_data\":[\"temporal_rs/compiled_data\"],\"zoneinfo64\":[\"dep:zoneinfo64\",\"timezone_provider/zoneinfo64\"]}}",
@@ -1453,6 +1442,7 @@
"tokio-rustls_0.26.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"argh\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.1\"},{\"features\":[\"pem\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"rustls/aws_lc_rs\"],\"brotli\":[\"rustls/brotli\"],\"default\":[\"logging\",\"tls12\",\"aws_lc_rs\"],\"early-data\":[],\"fips\":[\"rustls/fips\"],\"logging\":[\"rustls/logging\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"rustls/tls12\"],\"zlib\":[\"rustls/zlib\"]}}",
"tokio-stream_0.1.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"}],\"features\":{\"default\":[\"time\"],\"fs\":[\"tokio/fs\"],\"full\":[\"time\",\"net\",\"io-util\",\"fs\",\"sync\",\"signal\"],\"io-util\":[\"tokio/io-util\"],\"net\":[\"tokio/net\"],\"signal\":[\"tokio/signal\"],\"sync\":[\"tokio/sync\",\"tokio-util\"],\"time\":[\"tokio/time\"]}}",
"tokio-test_0.4.5": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.0\"},{\"features\":[\"rt\",\"sync\",\"time\",\"test-util\"],\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"name\":\"tokio-stream\",\"req\":\"^0.1.1\"}],\"features\":{}}",
+ "tokio-tungstenite_0.23.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"req\":\"^0.3.28\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"http1\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.11\"},{\"default_features\":false,\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.27.0\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.0\"},{\"default_features\":false,\"name\":\"tungstenite\",\"req\":\"^0.23.0\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26.0\"}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\",\"tokio-rustls\",\"stream\",\"tungstenite/__rustls-tls\",\"handshake\"],\"connect\":[\"stream\",\"tokio/net\",\"handshake\"],\"default\":[\"connect\",\"handshake\"],\"handshake\":[\"tungstenite/handshake\"],\"native-tls\":[\"native-tls-crate\",\"tokio-native-tls\",\"stream\",\"tungstenite/native-tls\",\"handshake\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\",\"tungstenite/native-tls-vendored\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"stream\":[],\"url\":[\"tungstenite/url\"]}}",
"tokio-util_0.7.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3.0\"},{\"name\":\"bytes\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"futures-sink\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.5\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.44.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\"}],\"features\":{\"__docs_rs\":[\"futures-util\"],\"codec\":[],\"compat\":[\"futures-io\"],\"default\":[],\"full\":[\"codec\",\"compat\",\"io-util\",\"time\",\"net\",\"rt\",\"join-map\"],\"io\":[],\"io-util\":[\"io\",\"tokio/rt\",\"tokio/io-util\"],\"join-map\":[\"rt\",\"hashbrown\"],\"net\":[\"tokio/net\"],\"rt\":[\"tokio/rt\",\"tokio/sync\",\"futures-util\"],\"time\":[\"tokio/time\",\"slab\"]}}",
"tokio_1.49.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.2.1\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-concurrency\",\"req\":\"^7.6.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"io-uring\",\"optional\":true,\"req\":\"^0.7.6\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"os-poll\",\"os-ext\"],\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"mio-aio\",\"req\":\"^1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.13.0\"},{\"default_features\":false,\"features\":[\"aio\",\"fs\",\"socket\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\",\"target\":\"cfg(unix)\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"signal-hook-registry\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(unix)\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.9\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"tokio-macros\",\"optional\":true,\"req\":\"~2.6.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\",\"target\":\"cfg(tokio_unstable)\"},{\"kind\":\"dev\",\"name\":\"tracing-mock\",\"req\":\"=0.1.0-beta.1\",\"target\":\"cfg(all(tokio_unstable, target_has_atomic = \\\"64\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.0\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.61\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Authorization\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"fs\":[],\"full\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"process\",\"rt\",\"rt-multi-thread\",\"signal\",\"sync\",\"time\"],\"io-std\":[],\"io-uring\":[\"dep:io-uring\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"dep:slab\"],\"io-util\":[\"bytes\"],\"macros\":[\"tokio-macros\"],\"net\":[\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"socket2\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_Security\",\"windows-sys/Win32_Storage_FileSystem\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_System_SystemServices\"],\"process\":[\"bytes\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Threading\",\"windows-sys/Win32_System_WindowsProgramming\"],\"rt\":[],\"rt-multi-thread\":[\"rt\"],\"signal\":[\"libc\",\"mio/os-poll\",\"mio/net\",\"mio/os-ext\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Console\"],\"sync\":[],\"taskdump\":[\"dep:backtrace\"],\"test-util\":[\"rt\",\"sync\",\"time\"],\"time\":[]}}",
"toml_0.5.11": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0.97\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[],\"preserve_order\":[\"indexmap\"]}}",
@@ -1490,6 +1480,7 @@
"try-lock_0.2.5": "{\"dependencies\":[],\"features\":{}}",
"ts-rs-macros_11.1.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.28\"},{\"name\":\"termcolor\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"no-serde-warnings\":[],\"serde-compat\":[\"termcolor\"]}}",
"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\"]}}",
+ "tungstenite_0.23.0": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.3.2\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\"},{\"name\":\"data-encoding\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.0\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"httparse\",\"optional\":true,\"req\":\"^1.3.4\"},{\"kind\":\"dev\",\"name\":\"input_buffer\",\"req\":\"^0.5.0\"},{\"name\":\"log\",\"req\":\"^0.4.8\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.3\"},{\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.5.5\"},{\"name\":\"thiserror\",\"req\":\"^1.0.23\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.1.0\"},{\"name\":\"utf-8\",\"req\":\"^0.7.5\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"handshake\":[\"data-encoding\",\"http\",\"httparse\",\"sha1\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]}}",
"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\":[]}}",
@@ -1526,7 +1517,6 @@
"v8_146.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"align-data\",\"req\":\"^0.1.0\"},{\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72\"},{\"kind\":\"dev\",\"name\":\"bindgen\",\"req\":\"^0.72\"},{\"name\":\"bitflags\",\"req\":\"^2.5\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"fslock\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"fslock\",\"req\":\"^0.2\"},{\"kind\":\"build\",\"name\":\"gzip-header\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"gzip-header\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"home\",\"req\":\"^0\"},{\"kind\":\"dev\",\"name\":\"home\",\"req\":\"^0\"},{\"kind\":\"build\",\"name\":\"miniz_oxide\",\"req\":\"^0.8.8\"},{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.8.8\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"zoneinfo64\"],\"name\":\"temporal_capi\",\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.96\"},{\"kind\":\"build\",\"name\":\"which\",\"req\":\"^6\"},{\"kind\":\"dev\",\"name\":\"which\",\"req\":\"^6\"}],\"features\":{\"default\":[\"use_custom_libcxx\"],\"use_custom_libcxx\":[],\"v8_enable_pointer_compression\":[],\"v8_enable_sandbox\":[\"v8_enable_pointer_compression\"],\"v8_enable_v8_checks\":[]}}",
"valuable_0.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"name\":\"valuable-derive\",\"optional\":true,\"req\":\"=0.1.1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"derive\":[\"valuable-derive\"],\"std\":[\"alloc\"]}}",
"vcpkg_0.2.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3.7\"}],\"features\":{}}",
- "version-compare_0.2.1": "{\"dependencies\":[],\"features\":{}}",
"version_check_0.9.5": "{\"dependencies\":[],\"features\":{}}",
"vt100_0.16.2": "{\"dependencies\":[{\"name\":\"itoa\",\"req\":\"^1.0.15\"},{\"features\":[\"term\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.30.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"},{\"kind\":\"dev\",\"name\":\"terminal_size\",\"req\":\"^0.4.2\"},{\"name\":\"unicode-width\",\"req\":\"^0.2.1\"},{\"name\":\"vte\",\"req\":\"^0.15.0\"}],\"features\":{}}",
"vte_0.15.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"req\":\"^0.7.2\"},{\"default_features\":false,\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2.3.3\"},{\"default_features\":false,\"name\":\"cursor-icon\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.7.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.160\"}],\"features\":{\"ansi\":[\"log\",\"cursor-icon\",\"bitflags\"],\"default\":[\"std\"],\"serde\":[\"dep:serde\"],\"std\":[\"memchr/std\"]}}",
@@ -1670,16 +1660,13 @@
"zerotrie_0.2.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"litemap\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"optional\":true,\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[],\"databake\":[\"dep:databake\",\"zerovec?/databake\"],\"default\":[],\"litemap\":[\"dep:litemap\",\"alloc\"],\"serde\":[\"dep:serde_core\",\"dep:litemap\",\"alloc\",\"litemap/serde\",\"zerovec?/serde\"],\"yoke\":[\"dep:yoke\"],\"zerofrom\":[\"dep:zerofrom\"]}}",
"zerovec-derive_0.11.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0.21\"}],\"features\":{}}",
"zerovec_0.11.5": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"xxhash64\"],\"name\":\"twox-hash\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec-derive\",\"optional\":true,\"req\":\"^0.11.1\"}],\"features\":{\"alloc\":[\"serde?/alloc\"],\"databake\":[\"dep:databake\"],\"derive\":[\"dep:zerovec-derive\"],\"hashmap\":[\"dep:twox-hash\",\"alloc\"],\"serde\":[\"dep:serde\"],\"std\":[],\"yoke\":[\"dep:yoke\"]}}",
- "zip_0.6.6": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8.2\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"crc32fast\",\"req\":\"^1.3.2\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.8\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0.23\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.5\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12.1\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.11.0\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.7\"},{\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.7\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.11.2\"}],\"features\":{\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate\",\"time\",\"zstd\"],\"deflate\":[\"flate2/rust_backend\"],\"deflate-miniz\":[\"flate2/default\"],\"deflate-zlib\":[\"flate2/zlib\"],\"unreserved\":[]}}",
"zip_2.4.2": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"req\":\"^1.4.1\",\"target\":\"cfg(fuzzing)\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crc32fast\",\"req\":\"^1.4\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"name\":\"deflate64\",\"optional\":true,\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"wasm_js\",\"std\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.1\"},{\"features\":[\"wasm_js\",\"std\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3.1\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"lzma-rs\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"memchr\",\"req\":\"^2.7\"},{\"default_features\":false,\"name\":\"nt-time\",\"optional\":true,\"req\":\"^0.10.6\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.15\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.37\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.37\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5\"},{\"name\":\"xz2\",\"optional\":true,\"req\":\"^0.1.7\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"},{\"name\":\"zopfli\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"_all-features\":[],\"_deflate-any\":[],\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\",\"getrandom\",\"zeroize\"],\"chrono\":[\"chrono/default\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate64\",\"deflate\",\"lzma\",\"time\",\"zstd\",\"xz\"],\"deflate\":[\"flate2/rust_backend\",\"deflate-zopfli\",\"deflate-flate2\"],\"deflate-flate2\":[\"_deflate-any\"],\"deflate-miniz\":[\"deflate\",\"deflate-flate2\"],\"deflate-zlib\":[\"flate2/zlib\",\"deflate-flate2\"],\"deflate-zlib-ng\":[\"flate2/zlib-ng\",\"deflate-flate2\"],\"deflate-zopfli\":[\"zopfli\",\"_deflate-any\"],\"lzma\":[\"lzma-rs/stream\"],\"nt-time\":[\"dep:nt-time\"],\"unreserved\":[],\"xz\":[\"dep:xz2\"]}}",
"zmij_1.0.19": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"opt-level\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"ryu\",\"req\":\"^1\"}],\"features\":{}}",
"zmij_1.0.21": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1.36\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"opt-level\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"ryu\",\"req\":\"^1\"}],\"features\":{}}",
"zoneinfo64_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"calendrical_calculations\",\"req\":\"^0.2.3\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.4\"},{\"default_features\":false,\"name\":\"icu_locale_core\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"features\":[\"tzdb-bundle-always\",\"std\"],\"kind\":\"dev\",\"name\":\"jiff\",\"req\":\"^0.2.15\"},{\"default_features\":false,\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"resb\",\"req\":\"^0.1.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.220\"}],\"features\":{\"chrono\":[\"dep:chrono\"]}}",
"zopfli_0.8.3": "{\"dependencies\":[{\"name\":\"bumpalo\",\"req\":\"^3.19.0\"},{\"default_features\":false,\"name\":\"crc32fast\",\"optional\":true,\"req\":\"^1.5.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.8.9\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"proptest-derive\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"simd-adler32\",\"optional\":true,\"req\":\"^0.3.7\"}],\"features\":{\"default\":[\"gzip\",\"std\",\"zlib\"],\"gzip\":[\"dep:crc32fast\"],\"nightly\":[\"crc32fast?/nightly\"],\"std\":[\"crc32fast?/std\",\"dep:log\",\"simd-adler32?/std\"],\"zlib\":[\"dep:simd-adler32\"]}}",
- "zstd-safe_5.0.2+zstd.1.5.2": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.21\"},{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.1\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}",
"zstd-safe_7.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.15\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"fat-lto\":[\"zstd-sys/fat-lto\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"seekable\":[\"zstd-sys/seekable\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"thin-lto\":[\"zstd-sys/thin-lto\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}",
"zstd-sys_2.0.16+zstd.1.5.7": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.45\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.28\"}],\"features\":{\"debug\":[],\"default\":[\"legacy\",\"zdict_builder\",\"bindgen\"],\"experimental\":[],\"fat-lto\":[],\"legacy\":[],\"no_asm\":[],\"no_wasm_shim\":[],\"non-cargo\":[],\"pkg-config\":[],\"seekable\":[],\"std\":[],\"thin\":[],\"thin-lto\":[],\"zdict_builder\":[],\"zstdmt\":[]}}",
- "zstd_0.11.2+zstd.1.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"humansize\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"zstd-safe\",\"req\":\"^5.0.1\"}],\"features\":{\"arrays\":[\"zstd-safe/arrays\"],\"bindgen\":[\"zstd-safe/bindgen\"],\"debug\":[\"zstd-safe/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-safe/experimental\"],\"legacy\":[\"zstd-safe/legacy\"],\"no_asm\":[\"zstd-safe/no_asm\"],\"pkg-config\":[\"zstd-safe/pkg-config\"],\"thin\":[\"zstd-safe/thin\"],\"wasm\":[],\"zdict_builder\":[\"zstd-safe/zdict_builder\"],\"zstdmt\":[\"zstd-safe/zstdmt\"]}}",
"zstd_0.13.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"humansize\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"partial-io\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"zstd-safe\",\"req\":\"^7.1.0\"}],\"features\":{\"arrays\":[\"zstd-safe/arrays\"],\"bindgen\":[\"zstd-safe/bindgen\"],\"debug\":[\"zstd-safe/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-safe/experimental\"],\"fat-lto\":[\"zstd-safe/fat-lto\"],\"legacy\":[\"zstd-safe/legacy\"],\"no_asm\":[\"zstd-safe/no_asm\"],\"pkg-config\":[\"zstd-safe/pkg-config\"],\"thin\":[\"zstd-safe/thin\"],\"thin-lto\":[\"zstd-safe/thin-lto\"],\"wasm\":[],\"zdict_builder\":[\"zstd-safe/zdict_builder\"],\"zstdmt\":[\"zstd-safe/zstdmt\"]}}",
"zune-core_0.4.12": "{\"dependencies\":[{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.52\"}],\"features\":{\"std\":[]}}",
"zune-core_0.5.1": "{\"dependencies\":[{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"std\":[]}}",
diff --git a/README.md b/README.md
index 5cc7fd495..318f0a0eb 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,196 @@
-npm i -g @openai/codex or brew install --cask codex
-Codex CLI is a coding agent from OpenAI that runs locally on your computer.
-
-
-
-
-If you want Codex in your code editor (VS Code, Cursor, Windsurf), install in your IDE.
-If you want the desktop app experience, run codex app or visit the Codex App page .
-If you are looking for the cloud-based agent from OpenAI, Codex Web , go to chatgpt.com/codex .
-
----
+
+
+# Codex Enhanced
+
+> The Codex distribution built for real 24/7 use.
+
+[中文版本](./README.zh-CN.md) · [Website](https://codex-enhanced.com) · [Workflows](./docs/workflows.md) · [Slash Commands](./docs/slash_commands.md) · [Structured Input UI](./docs/tui-request-user-input.md)
+
+
+
+`codex-enhanced` is a Codex distribution built on top of the OpenAI Codex CLI Rust stack. It is focused less on prompt theater and more on turning Codex into an operator surface that can stay online across accounts, sessions, workflows, and external message channels.
+
+If you need a terminal chatbot, the base Codex experience is already strong. This distribution is for the next step: keeping Codex running as a controllable workspace system instead of a single ephemeral chat loop.
+
+## Why codex-enhanced
+
+Most AI CLI projects compete on model access and UI polish.
+
+`codex-enhanced` moves the investment somewhere else:
+
+- multi-subscription account management instead of manual env switching
+- multi-profile routing and fallback instead of a single fragile default
+- resumable sessions instead of repeated context rebuilds
+- workflow triggers and background jobs instead of one-turn-at-a-time operation
+- Feishu bridge entrypoints instead of terminal-only interaction
+- lower-noise operator UX instead of more surface-level prompt ceremony
+
+That is the core idea: make Codex feel less like a terminal chatbot and more like a persistent control surface.
+
+## What You Can Do
+
+| Capability | Entry point | What it enables |
+| --- | --- | --- |
+| Multi-profile routing | `/profile` | Switch named profiles at runtime, manage fallback routes, and recover from rate limits or auth failures without rewriting local environment state. |
+| Workflow orchestration | `/workflow` | Manage `.codex/workflows/*.yaml`, run jobs manually, and attach triggers such as `before_turn`, `after_turn`, `interval`, `cron`, and `file_watch`. |
+| Session insight report | `/insight` | Scan local Codex sessions and generate an offline HTML report under `~/.codex/reports/` for rollout analysis and drill-down. |
+| Session continuity | `/resume` | Reconnect to saved work instead of reconstructing long-running context from scratch. |
+| External message bridge | `/clawbot` | Bind workspace-local Feishu sessions to Codex threads, capture unread messages, and forward final replies back out. |
+| UI and alignment control | `/settings`, `question`, keyboard chords | Reduce noise, collect structured answers in the TUI, and keep operator interactions explicit. |
## Quickstart
-### Installing and running Codex CLI
+Install from PyPI:
-Install globally with your preferred package manager:
+```bash
+pip3 install -U codex-enhanced
+codex-enhanced
+```
+
+Run from source in this repository:
-```shell
-# Install using npm
-npm install -g @openai/codex
+```bash
+just codex
```
-```shell
-# Install using Homebrew
-brew install --cask codex
+`just codex` runs `cargo run --bin codex -- ...` from the `codex-rs` workspace, which is the fastest way to inspect or develop the Rust TUI locally.
+
+## Typical Operator Flows
+
+### 1. Route traffic across multiple accounts and profiles
+
+`/profile` opens a dedicated management panel for:
+
+- named profiles
+- runtime switching
+- fallback routes
+- switching policies for rate limits, auth failures, and service overload
+
+This is the difference between "change a key and restart the tool" and "keep the operator surface online."
+
+### 2. Turn conversations into jobs
+
+`/workflow` manages workflow definitions directly from `.codex/workflows/*.yaml`.
+
+Supported trigger families include:
+
+- `before_turn`
+- `after_turn`
+- `manual`
+- `file_watch`
+- `idle`
+- `interval`
+- `cron`
+
+That lets Codex participate in repeatable automation loops instead of only answering the current prompt.
+
+Minimal example:
+
+```yaml
+name: director
+
+triggers:
+ - id: pulse
+ type: interval
+ every: 30m
+ enabled: true
+ jobs: [notify]
+
+jobs:
+ notify:
+ enabled: true
+ context: ephemeral
+ response: assistant
+ steps:
+ - prompt: |
+ Send a concise update on the current workspace state.
```
-Then simply run `codex` to get started.
+Documentation:
+
+- [`docs/workflows.md`](./docs/workflows.md)
+
+### 3. Resume work instead of restarting it
+
+`/resume` and the underlying thread/session plumbing let you reconnect to saved work instead of paying the cost of rebuilding context every time a session is interrupted.
+
+This matters once Codex is doing real work over hours or days rather than a single short exchange.
+
+### 4. Bring Feishu into the same loop
+
+`/clawbot` connects workspace-local Feishu sessions, thread binding, unread message queues, and reply forwarding into the same runtime.
+
+In practice, that means:
+
+- a Feishu chat can be bound to the current Codex thread
+- inbound messages can enter the active workspace loop
+- final replies can be sent back to Feishu
+- session and binding state stays local to the workspace runtime
+
+## What Ships Today
+
+These capabilities are already implemented in the repository:
+
+- multi-subscription account management and runtime account display
+- multi-profile API management and `/profile` route switching
+- `/workflow` task orchestration
+- `/resume` for saved sessions
+- `/settings` for UI information control
+- `/clawbot` for Feishu send and receive flows
+- stronger `question`-based alignment interactions
+- keyboard chord support
+- PyPI packaging and release flow for `codex-enhanced`
+
+## Inspectable by Design
+
+This project is intentionally built so the important runtime state stays visible:
+
+- profile routing state lives in `accounts/profile-router.json`
+- workflows live in `.codex/workflows/*.yaml`
+- clawbot state is stored under `.codex/clawbot/`
+- structured operator answers are collected through the TUI `question` flow instead of being guessed from ambiguous free text
+
+This distribution is opinionated, but it is not trying to hide the system from the operator.
+
+## Repository Map
+
+If you want to inspect or extend the project, start here:
+
+- [`codex-rs/`](./codex-rs) contains the Rust workspace, including the CLI, TUI, workflow support, app-server pieces, and clawbot integration
+- [`sdk/python-runtime-enhanced/`](./sdk/python-runtime-enhanced) contains the Python wheel packaging for `codex-enhanced`
+- [`docs/workflows.md`](./docs/workflows.md) explains workflow files, triggers, and job management
+- [`docs/slash_commands.md`](./docs/slash_commands.md) documents TUI slash commands including `/insight`
+- [`docs/tui-request-user-input.md`](./docs/tui-request-user-input.md) explains the structured input overlay used for `question`
+
+## Capability Boundaries
+
+### What it is built to solve
-
-You can also go to the latest GitHub Release and download the appropriate binary for your platform.
+This distribution is strong at connecting the agent to real operating workflows.
-Each GitHub Release contains many executables, but in practice, you likely want one of these:
+Current strengths:
-- macOS
- - Apple Silicon/arm64: `codex-aarch64-apple-darwin.tar.gz`
- - x86_64 (older Mac hardware): `codex-x86_64-apple-darwin.tar.gz`
-- Linux
- - x86_64: `codex-x86_64-unknown-linux-musl.tar.gz`
- - arm64: `codex-aarch64-unknown-linux-musl.tar.gz`
+- multi-account and multi-profile routing
+- long-session recovery and continuity
+- workspace-local workflow orchestration
+- Feishu clawbot integration
+- local TUI information shaping and visibility control
+- stronger alignment flows for human-in-the-loop operation
-Each archive contains a single entry with the platform baked into the name (e.g., `codex-x86_64-unknown-linux-musl`), so you likely want to rename it to `codex` after extracting it.
+### What it is not trying to be
-
+- a replacement for every official hosted or distributed Codex surface
+- a general-purpose IM automation hub beyond the current Feishu focus
+- a zero-configuration black box for business workflow automation
-### Using Codex with your ChatGPT plan
+## Project Attribution
-Run `codex` and select **Sign in with ChatGPT**. We recommend signing into your ChatGPT account to use Codex as part of your Plus, Pro, Business, Edu, or Enterprise plan. [Learn more about what's included in your ChatGPT plan](https://help.openai.com/en/articles/11369540-codex-in-chatgpt).
+This project builds on the OpenAI Codex CLI Rust, TUI, and app-server foundation, then pushes harder on the parts that matter in sustained use: account operations, session continuity, workflows, Feishu entrypoints, lower-noise UI, and operator ergonomics.
-You can also use Codex with an API key, but this requires [additional setup](https://developers.openai.com/codex/auth#sign-in-with-an-api-key).
+If you only need a Codex that chats in a terminal, the official distribution is already enough.
-## Docs
+If you need a Codex that can stay online across accounts, inputs, tasks, and long-running sessions, that is the point of this distribution.
-- [**Codex Documentation**](https://developers.openai.com/codex)
-- [**Contributing**](./docs/contributing.md)
-- [**Installing & building**](./docs/install.md)
-- [**Open source fund**](./docs/open-source-fund.md)
+## License
-This repository is licensed under the [Apache-2.0 License](LICENSE).
+Apache-2.0
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 000000000..4d5cc61be
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,196 @@
+
+
+# Codex Enhanced
+
+> 真正 24/7 使用的 Codex 发行版。
+
+[English](./README.md) · [Website](https://codex-enhanced.com) · [工作流文档](./docs/workflows.md) · [Slash Command 文档](./docs/slash_commands.md) · [结构化输入 UI](./docs/tui-request-user-input.md)
+
+
+
+`codex-enhanced` 是一个基于 OpenAI Codex CLI Rust 技术栈继续演进的 Codex 发行版。它的重点不是再包一层 prompt,而是把 Codex 变成一个可以跨账户、跨会话、跨工作流、跨外部消息入口持续在线的 operator surface。
+
+如果你只需要一个终端聊天工具,基础 Codex 体验已经足够强。这个发行版要解决的是下一层问题:让 Codex 不只是一次性的对话循环,而是一个可持续运转的工作台。
+
+## 为什么是 codex-enhanced
+
+大多数 AI CLI 项目主要在卷模型接入和 UI 打磨。
+
+`codex-enhanced` 把工程投入放在另一侧:
+
+- 多 subscription 账户管理,而不是手动切环境变量
+- 多 profile 路由和 fallback,而不是只有一个脆弱默认值
+- 会话续接,而不是反复重建上下文
+- 工作流触发器和后台任务,而不是永远一问一答
+- 飞书桥接入口,而不是只接受终端输入
+- 更低噪音的 operator UX,而不是继续堆 prompt 仪式感
+
+核心目标只有一个:让 Codex 更像持续在线的控制台,而不是终端里的聊天框。
+
+## 你可以做什么
+
+| 能力 | 入口 | 能解决什么 |
+| --- | --- | --- |
+| 多 profile 路由 | `/profile` | 在运行时切换命名 profile、管理 fallback route,并在限流或鉴权失败时继续运转,而不是改完配置再重启。 |
+| 工作流编排 | `/workflow` | 直接管理 `.codex/workflows/*.yaml`,手动运行 job,或者挂接 `before_turn`、`after_turn`、`interval`、`cron`、`file_watch` 等触发器。 |
+| 会话洞察报告 | `/insight` | 扫描本地 Codex session,并在 `~/.codex/reports/` 下生成离线 HTML 分析报告,便于回看 rollout 和逐层钻取。 |
+| 会话连续性 | `/resume` | 把保存过的工作续上,而不是每次从零重建长上下文。 |
+| 外部消息桥接 | `/clawbot` | 把 workspace-local 的飞书会话绑定到 Codex thread,接收未读消息并把最终回复发回外部。 |
+| UI 与对齐控制 | `/settings`、`question`、键盘 chord | 降低界面噪音,在 TUI 中收集结构化答案,并让人工参与点更明确。 |
+
+## Quickstart
+
+通过 PyPI 安装:
+
+```bash
+pip3 install -U codex-enhanced
+codex-enhanced
+```
+
+在当前仓库里直接从源码运行:
+
+```bash
+just codex
+```
+
+`just codex` 实际执行的是 `codex-rs` 工作区下的 `cargo run --bin codex -- ...`,这是本地调试和验证 Rust TUI 的最快路径。
+
+## 典型使用流
+
+### 1. 在多个账户和 profile 之间路由流量
+
+`/profile` 会打开独立管理面板,支持:
+
+- 命名 profile
+- 当前 runtime 切换
+- fallback route
+- 在限流、鉴权失败、服务过载时按策略切换 profile
+
+这和“改个 key 然后重开工具”是两种完全不同的操作体验。
+
+### 2. 把对话变成可编排 job
+
+`/workflow` 直接管理 `.codex/workflows/*.yaml` 中的工作流定义。
+
+目前支持的触发类型包括:
+
+- `before_turn`
+- `after_turn`
+- `manual`
+- `file_watch`
+- `idle`
+- `interval`
+- `cron`
+
+这意味着 Codex 不只是响应当前输入,还可以进入可重复执行的自动化闭环。
+
+最小示例:
+
+```yaml
+name: director
+
+triggers:
+ - id: pulse
+ type: interval
+ every: 30m
+ enabled: true
+ jobs: [notify]
+
+jobs:
+ notify:
+ enabled: true
+ context: ephemeral
+ response: assistant
+ steps:
+ - prompt: |
+ Send a concise update on the current workspace state.
+```
+
+相关文档:
+
+- [`docs/workflows.md`](./docs/workflows.md)
+
+### 3. 续上工作,而不是反复重开
+
+`/resume` 和底层的 thread/session 基础设施允许你把保存过的工作重新接起来,而不是每次都付出重建上下文的成本。
+
+当 Codex 开始承担按小时或按天推进的任务时,这一点会非常重要。
+
+### 4. 把飞书接进同一个闭环
+
+`/clawbot` 把 workspace-local 的飞书会话、线程绑定、未读消息队列和回复回传放进同一个运行时。
+
+具体来说,它支持:
+
+- 把飞书会话绑定到当前 Codex thread
+- 让外部消息进入当前 workspace 的执行闭环
+- 把最终回复发回飞书
+- 让 session 和 binding 状态保持在 workspace 本地
+
+## 当前已经落地的能力
+
+下面这些能力都已经在仓库中实现:
+
+- 多 subscription 账户管理和 runtime account 展示
+- 多 profile API 管理和 `/profile` 路由切换
+- `/workflow` 任务编排
+- `/resume` 恢复已保存会话
+- `/settings` 控制 UI 展示信息
+- `/clawbot` 对接飞书收发消息
+- 更强的 `question` 式对齐交互
+- 键盘 chord 支持
+- `codex-enhanced` 的 PyPI 打包与发布流程
+
+## 可观测设计
+
+这个项目有意把关键状态保持为可检查、可定位的本地文件:
+
+- profile 路由状态保存在 `accounts/profile-router.json`
+- workflow 定义直接保存在 `.codex/workflows/*.yaml`
+- clawbot 相关状态保存在 `.codex/clawbot/`
+- operator 的结构化回答通过 TUI 中的 `question` 流收集,而不是靠自由文本猜测
+
+它是有观点的,但不是黑盒。
+
+## 仓库导览
+
+如果你要继续查看实现或扩展能力,可以从这些位置开始:
+
+- [`codex-rs/`](./codex-rs) 是 Rust 工作区,包含 CLI、TUI、workflow、app-server 和 clawbot 集成
+- [`sdk/python-runtime-enhanced/`](./sdk/python-runtime-enhanced) 是 `codex-enhanced` 的 Python wheel 打包目录
+- [`docs/workflows.md`](./docs/workflows.md) 说明 workflow 文件、trigger 和 job 管理方式
+- [`docs/slash_commands.md`](./docs/slash_commands.md) 说明 TUI slash commands,包括 `/insight`
+- [`docs/tui-request-user-input.md`](./docs/tui-request-user-input.md) 说明 `question` 使用的结构化输入浮层
+
+## 能力边界
+
+### 它主要解决什么
+
+这个发行版的强项,是把 agent 接进真实可操作的工作流。
+
+当前重点能力:
+
+- 多账户和多 profile 路由
+- 长会话恢复与连续性
+- workspace-local workflow orchestration
+- Feishu clawbot 集成
+- 本地 TUI 信息展示裁剪和可见性控制
+- 更强的人在环对齐交互
+
+### 它不打算解决什么
+
+- 替代官方原版的全部托管和分发形态
+- 在飞书之外直接变成通用 IM 自动化中台
+- 把业务流程自动化做成零配置黑盒
+
+## 项目说明
+
+这个项目不是从零开始。它建立在 OpenAI Codex CLI 的 Rust、TUI 和 app-server 基础之上,然后把精力进一步放到长期使用更痛的部分:账户运营、会话连续性、workflow、飞书入口、更低噪音的 UI 和 operator ergonomics。
+
+如果你只需要一个在终端里聊天的 Codex,官方原版已经够用。
+
+如果你需要一个能长期在线,能跨账户、跨入口、跨任务持续运转的 Codex,这就是这个发行版存在的理由。
+
+## License
+
+Apache-2.0
diff --git a/codex-rs/.gitignore b/codex-rs/.gitignore
index e31566047..b8563bddf 100644
--- a/codex-rs/.gitignore
+++ b/codex-rs/.gitignore
@@ -6,3 +6,5 @@
# Value of CARGO_TARGET_DIR when using .devcontainer/devcontainer.json.
/target-arm64/
+
+.codex/
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 76188da5b..167cfcfc3 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -449,7 +449,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -801,7 +801,7 @@ dependencies = [
"sha1",
"sync_wrapper",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tower",
"tower-layer",
"tower-service",
@@ -1372,7 +1372,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
- "unicode-width 0.1.14",
+ "unicode-width 0.2.1",
]
[[package]]
@@ -1428,10 +1428,10 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tokio-test",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tokio-util",
"tracing",
- "tungstenite",
+ "tungstenite 0.27.0",
"url",
"wiremock",
]
@@ -1499,7 +1499,7 @@ dependencies = [
"tempfile",
"time",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tokio-util",
"toml 0.9.11+spec-1.1.0",
"tracing",
@@ -1527,7 +1527,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"toml 0.9.11+spec-1.1.0",
"tracing",
"url",
@@ -1576,7 +1576,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
- "tungstenite",
+ "tungstenite 0.27.0",
"url",
"uuid",
]
@@ -1672,6 +1672,20 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "codex-clawbot"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "openlark",
+ "pretty_assertions",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "toml 0.9.11+spec-1.1.0",
+]
+
[[package]]
name = "codex-cli"
version = "0.0.0"
@@ -1740,10 +1754,10 @@ dependencies = [
"opentelemetry",
"opentelemetry_sdk",
"pretty_assertions",
- "rand 0.9.3",
+ "rand 0.9.2",
"reqwest",
"rustls",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"serde",
"serde_json",
@@ -1969,7 +1983,6 @@ dependencies = [
"core_test_support",
"crypto_box",
"csv",
- "ctor 0.6.3",
"dirs",
"dunce",
"ed25519-dalek",
@@ -1990,7 +2003,7 @@ dependencies = [
"opentelemetry_sdk",
"predicates",
"pretty_assertions",
- "rand 0.9.3",
+ "rand 0.9.2",
"regex-lite",
"reqwest",
"rmcp",
@@ -2006,7 +2019,7 @@ dependencies = [
"test-log",
"thiserror 2.0.18",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tokio-util",
"toml 0.9.11+spec-1.1.0",
"toml_edit 0.24.0+spec-1.1.0",
@@ -2164,7 +2177,7 @@ dependencies = [
"test-case",
"thiserror 2.0.18",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tracing",
"uuid",
]
@@ -2384,7 +2397,7 @@ dependencies = [
"once_cell",
"os_info",
"pretty_assertions",
- "rand 0.9.3",
+ "rand 0.9.2",
"regex-lite",
"reqwest",
"serde",
@@ -2589,7 +2602,7 @@ dependencies = [
"strum_macros 0.28.0",
"thiserror 2.0.18",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@@ -2780,7 +2793,7 @@ dependencies = [
"codex-keyring-store",
"keyring",
"pretty_assertions",
- "rand 0.9.3",
+ "rand 0.9.2",
"regex",
"schemars 0.8.22",
"serde",
@@ -2946,6 +2959,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-arg0",
"codex-chatgpt",
+ "codex-clawbot",
"codex-cli",
"codex-cloud-requirements",
"codex-config",
@@ -2975,6 +2989,7 @@ dependencies = [
"codex-utils-fuzzy-match",
"codex-utils-oss",
"codex-utils-pty",
+ "codex-utils-rustls-provider",
"codex-utils-sandbox-summary",
"codex-utils-sleep-inhibitor",
"codex-utils-string",
@@ -2986,15 +3001,18 @@ dependencies = [
"diffy",
"dirs",
"dunce",
+ "humantime",
"image",
+ "indexmap 2.13.0",
"insta",
"itertools 0.14.0",
"lazy_static",
"libc",
+ "notify",
"pathdiff",
"pretty_assertions",
"pulldown-cmark",
- "rand 0.9.3",
+ "rand 0.9.2",
"ratatui",
"ratatui-macros",
"regex-lite",
@@ -3002,6 +3020,7 @@ dependencies = [
"rmcp",
"serde",
"serde_json",
+ "serde_yaml",
"serial_test",
"shlex",
"strum 0.27.2",
@@ -3484,7 +3503,6 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
- "ctor 0.6.3",
"futures",
"notify",
"opentelemetry",
@@ -3496,7 +3514,7 @@ dependencies = [
"shlex",
"tempfile",
"tokio",
- "tokio-tungstenite",
+ "tokio-tungstenite 0.28.0",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
@@ -4504,7 +4522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5066,7 +5084,7 @@ dependencies = [
"gobject-sys",
"libc",
"system-deps",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5293,7 +5311,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
- "rand 0.9.3",
+ "rand 0.9.2",
"ring",
"thiserror 2.0.18",
"tinyvec",
@@ -5315,7 +5333,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
- "rand 0.9.3",
+ "rand 0.9.2",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
@@ -5423,6 +5441,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+[[package]]
+name = "humantime"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
+
[[package]]
name = "hyper"
version = "1.8.1"
@@ -5456,7 +5480,7 @@ dependencies = [
"hyper",
"hyper-util",
"rustls",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@@ -5510,7 +5534,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2 0.6.2",
"system-configuration",
"tokio",
"tower-service",
@@ -5596,7 +5620,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.58.0",
+ "windows-core 0.62.2",
]
[[package]]
@@ -6016,7 +6040,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -6246,6 +6270,17 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+[[package]]
+name = "lark-websocket-protobuf"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79d4b1f8c37d37efd353c1b116df0f98ff21c8bcb216f67ab026dd2fd2b9ab81"
+dependencies = [
+ "bytes",
+ "prost 0.13.5",
+ "prost-build 0.12.6",
+]
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -7308,6 +7343,143 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+[[package]]
+name = "openlark"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad168187bbb6fc22aa95df6c684b4d1d3345ca36c16f342270e75dbf869b3d8"
+dependencies = [
+ "chrono",
+ "openlark-auth",
+ "openlark-client",
+ "openlark-communication",
+ "openlark-core",
+ "openlark-docs",
+ "serde",
+ "serde_json",
+ "serde_repr",
+]
+
+[[package]]
+name = "openlark-auth"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fee5a868321d6307fbbda4dd3a127a50b2d90375072bdbc5cbd1e633a545db4"
+dependencies = [
+ "anyhow",
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "openlark-core",
+ "rand 0.8.5",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "url",
+ "urlencoding",
+ "uuid",
+]
+
+[[package]]
+name = "openlark-client"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da8bcf21d0d5e5beef862a29c794679d50d5d376e9f9ea4a851e081356628910"
+dependencies = [
+ "chrono",
+ "futures-util",
+ "lark-websocket-protobuf",
+ "log",
+ "openlark-auth",
+ "openlark-communication",
+ "openlark-core",
+ "openlark-docs",
+ "prost 0.13.5",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-tungstenite 0.23.1",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "openlark-communication"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d102d0ad5f63ebbe729e7f513bce18cb0e45bd0da6efb346e8b281ea5b619b1f"
+dependencies = [
+ "openlark-core",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tracing",
+]
+
+[[package]]
+name = "openlark-core"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336a10748627ee7c57fc2d80d686b210065670bc7c01a17cacd6e85fd7548975"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "futures-util",
+ "hmac",
+ "http 1.4.0",
+ "num_cpus",
+ "quick_cache",
+ "rand 0.8.5",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "sha2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "urlencoding",
+ "uuid",
+]
+
+[[package]]
+name = "openlark-docs"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff30823a5df1ae99b2316ffeb4548fb9c90f5c9a145d719626b77cfafa0d3e07"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.22.1",
+ "chrono",
+ "futures",
+ "futures-util",
+ "log",
+ "once_cell",
+ "openlark-core",
+ "rand 0.8.5",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "thiserror 2.0.18",
+ "tokio",
+ "url",
+ "urlencoding",
+ "uuid",
+]
+
[[package]]
name = "openssl"
version = "0.10.75"
@@ -7461,7 +7633,7 @@ dependencies = [
"futures-util",
"opentelemetry",
"percent-encoding",
- "rand 0.9.3",
+ "rand 0.9.2",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
@@ -7506,7 +7678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8010,7 +8182,7 @@ checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
dependencies = [
"bitflags 2.10.0",
"num-traits",
- "rand 0.9.3",
+ "rand 0.9.2",
"rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax 0.8.8",
@@ -8027,6 +8199,16 @@ dependencies = [
"prost-derive 0.12.6",
]
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive 0.13.5",
+]
+
[[package]]
name = "prost"
version = "0.14.3"
@@ -8090,6 +8272,19 @@ dependencies = [
"syn 2.0.114",
]
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
[[package]]
name = "prost-derive"
version = "0.14.3"
@@ -8180,6 +8375,18 @@ dependencies = [
"serde",
]
+[[package]]
+name = "quick_cache"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a70b1b8b47e31d0498ecbc3c5470bb931399a8bfed1fd79d1717a61ce7f96e3"
+dependencies = [
+ "ahash",
+ "equivalent",
+ "hashbrown 0.16.1",
+ "parking_lot",
+]
+
[[package]]
name = "quinn"
version = "0.11.9"
@@ -8193,7 +8400,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
- "socket2 0.5.10",
+ "socket2 0.6.2",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -8209,7 +8416,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
- "rand 0.9.3",
+ "rand 0.9.2",
"ring",
"rustc-hash 2.1.1",
"rustls",
@@ -8230,9 +8437,9 @@ dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2 0.6.2",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -8341,7 +8548,7 @@ dependencies = [
"rama-http-types",
"rama-net",
"rama-utils",
- "rand 0.9.3",
+ "rand 0.9.2",
"serde",
"serde_html_form",
"serde_json",
@@ -8411,7 +8618,7 @@ dependencies = [
"rama-macros",
"rama-net",
"rama-utils",
- "rand 0.9.3",
+ "rand 0.9.2",
"serde",
"sha1",
]
@@ -8439,7 +8646,7 @@ dependencies = [
"rama-error",
"rama-macros",
"rama-utils",
- "rand 0.9.3",
+ "rand 0.9.2",
"serde",
"serde_json",
"sync_wrapper",
@@ -8513,7 +8720,7 @@ dependencies = [
"rama-http-types",
"rama-net",
"rama-utils",
- "rand 0.9.3",
+ "rand 0.9.2",
"tokio",
]
@@ -8530,7 +8737,7 @@ dependencies = [
"rama-utils",
"rcgen",
"rustls",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@@ -8593,9 +8800,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.9.3"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
@@ -8835,12 +9042,13 @@ dependencies = [
"js-sys",
"log",
"mime",
+ "mime_guess",
"native-tls",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"serde",
"serde_json",
@@ -8910,7 +9118,7 @@ dependencies = [
"pastey",
"pin-project-lite",
"process-wrap",
- "rand 0.9.3",
+ "rand 0.9.2",
"reqwest",
"rmcp-macros",
"schemars 1.2.1",
@@ -9074,7 +9282,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -9093,6 +9301,19 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
+dependencies = [
+ "openssl-probe 0.1.6",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 2.11.1",
+]
+
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
@@ -9105,6 +9326,15 @@ dependencies = [
"security-framework 3.5.1",
]
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
@@ -9117,9 +9347,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.103.12"
+version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
+checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"aws-lc-rs",
"ring",
@@ -9494,7 +9724,7 @@ version = "0.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26ab054c34b87f96c3e4701bea1888317cde30cc7e4a6136d2c48454ab96661c"
dependencies = [
- "rand 0.9.3",
+ "rand 0.9.2",
"sentry-types",
"serde",
"serde_json",
@@ -9542,7 +9772,7 @@ checksum = "eecbd63e9d15a26a40675ed180d376fcb434635d2e33de1c24003f61e3e2230d"
dependencies = [
"debugid",
"hex",
- "rand 0.9.3",
+ "rand 0.9.2",
"serde",
"serde_json",
"thiserror 2.0.18",
@@ -10533,7 +10763,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -10919,6 +11149,22 @@ dependencies = [
"tokio-stream",
]
+[[package]]
+name = "tokio-tungstenite"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-native-certs 0.7.3",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite 0.23.0",
+]
+
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
@@ -10927,11 +11173,11 @@ dependencies = [
"futures-util",
"log",
"rustls",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
"tokio-rustls",
- "tungstenite",
+ "tungstenite 0.27.0",
]
[[package]]
@@ -11041,7 +11287,7 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
- "rustls-native-certs",
+ "rustls-native-certs 0.8.3",
"socket2 0.6.2",
"sync_wrapper",
"tokio",
@@ -11055,9 +11301,9 @@ dependencies = [
[[package]]
name = "tonic-build"
-version = "0.14.3"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc"
+checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
dependencies = [
"prettyplease",
"proc-macro2",
@@ -11346,6 +11592,26 @@ dependencies = [
"termcolor",
]
+[[package]]
+name = "tungstenite"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.4.0",
+ "httparse",
+ "log",
+ "rand 0.8.5",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
+]
+
[[package]]
name = "tungstenite"
version = "0.27.0"
@@ -11358,7 +11624,7 @@ dependencies = [
"http 1.4.0",
"httparse",
"log",
- "rand 0.9.3",
+ "rand 0.9.2",
"rustls",
"rustls-pki-types",
"sha1",
diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml
index c8f6b9e5a..3263dddd8 100644
--- a/codex-rs/Cargo.toml
+++ b/codex-rs/Cargo.toml
@@ -21,6 +21,7 @@ members = [
"cloud-tasks-client",
"cloud-tasks-mock-client",
"cli",
+ "clawbot",
"collaboration-mode-templates",
"connectors",
"config",
@@ -120,6 +121,7 @@ codex-async-utils = { path = "async-utils" }
codex-backend-client = { path = "backend-client" }
codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }
+codex-clawbot = { path = "clawbot" }
codex-client = { path = "codex-client" }
codex-collaboration-mode-templates = { path = "collaboration-mode-templates" }
codex-cloud-requirements = { path = "cloud-requirements" }
diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md
index 2fece4454..8b29aef44 100644
--- a/codex-rs/app-server/README.md
+++ b/codex-rs/app-server/README.md
@@ -1096,9 +1096,9 @@ Order of messages:
UI guidance for IDEs: surface an approval dialog as soon as the request arrives. The turn will proceed after the server receives a response to the approval request. The terminal `item/completed` notification will be sent with the appropriate status.
-### request_user_input
+### question / request_user_input
-When the client responds to `item/tool/requestUserInput`, the server emits `serverRequest/resolved` with `{ threadId, requestId }`. If the pending request is cleared by turn start, turn completion, or turn interruption before the client answers, the server emits the same notification for that cleanup.
+When the client responds to `item/tool/requestUserInput`, whether it originated from the `question` tool or the legacy `request_user_input` tool, the server emits `serverRequest/resolved` with `{ threadId, requestId }`. If the pending request is cleared by turn start, turn completion, or turn interruption before the client answers, the server emits the same notification for that cleanup.
### MCP server elicitations
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 e8682d732..712124abc 100644
--- a/codex-rs/app-server/tests/suite/v2/turn_start.rs
+++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs
@@ -839,7 +839,11 @@ async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()> {
let payload = request.body_json();
assert_eq!(payload["model"].as_str(), Some("mock-model-collab"));
let payload_text = payload.to_string();
- assert!(payload_text.contains("The `request_user_input` tool is available in Default mode."));
+ assert!(
+ payload_text.contains(
+ "The `question` and `request_user_input` tools are available in Default mode."
+ )
+ );
Ok(())
}
@@ -924,7 +928,11 @@ async fn turn_start_uses_thread_feature_overrides_for_collaboration_mode_instruc
let request = response_mock.single_request();
let payload_text = request.body_json().to_string();
- assert!(payload_text.contains("The `request_user_input` tool is available in Default mode."));
+ assert!(
+ payload_text.contains(
+ "The `question` and `request_user_input` tools are available in Default mode."
+ )
+ );
Ok(())
}
diff --git a/codex-rs/clawbot/BUILD.bazel b/codex-rs/clawbot/BUILD.bazel
new file mode 100644
index 000000000..f559c0e78
--- /dev/null
+++ b/codex-rs/clawbot/BUILD.bazel
@@ -0,0 +1,6 @@
+load("//:defs.bzl", "codex_rust_crate")
+
+codex_rust_crate(
+ name = "clawbot",
+ crate_name = "codex_clawbot",
+)
diff --git a/codex-rs/clawbot/Cargo.toml b/codex-rs/clawbot/Cargo.toml
new file mode 100644
index 000000000..2b1543a52
--- /dev/null
+++ b/codex-rs/clawbot/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "codex-clawbot"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+
+[lib]
+name = "codex_clawbot"
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+anyhow = { workspace = true }
+openlark = { version = "0.15.0", default-features = false, features = ["auth", "communication", "docs-bitable", "websocket"] }
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
+tokio = { workspace = true, features = ["rt", "sync", "time"] }
+toml = { workspace = true }
+
+[dev-dependencies]
+pretty_assertions = { workspace = true }
+tempfile = { workspace = true }
diff --git a/codex-rs/clawbot/src/bin/feishu-bitable-smoke.rs b/codex-rs/clawbot/src/bin/feishu-bitable-smoke.rs
new file mode 100644
index 000000000..59216adcd
--- /dev/null
+++ b/codex-rs/clawbot/src/bin/feishu-bitable-smoke.rs
@@ -0,0 +1,73 @@
+use std::path::PathBuf;
+use std::time::Duration;
+
+use anyhow::Context;
+use anyhow::Result;
+use codex_clawbot::ClawbotRuntime;
+use open_lark::openlark_client;
+use open_lark::openlark_core::api::ApiRequest;
+use open_lark::openlark_core::http::Transport;
+use serde_json::Value;
+
+fn main() -> Result<()> {
+ let workspace = std::env::args()
+ .nth(1)
+ .map(PathBuf::from)
+ .unwrap_or_else(|| PathBuf::from("~/clawbot"));
+ let workspace = expand_home(workspace);
+ let runtime = ClawbotRuntime::load(workspace.clone())
+ .with_context(|| format!("failed to load workspace {}", workspace.display()))?;
+ let feishu = runtime
+ .snapshot()
+ .config
+ .feishu
+ .clone()
+ .context("missing feishu config")?;
+ let coordination = feishu
+ .coordination
+ .clone()
+ .context("missing feishu coordination config")?;
+
+ let rt = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .context("failed to build tokio runtime")?;
+ rt.block_on(async move {
+ let config = openlark_client::Config::builder()
+ .app_id(feishu.app_id)
+ .app_secret(feishu.app_secret)
+ .timeout(Duration::from_secs(30))
+ .build()
+ .context("failed to build websocket config")?
+ .build_core_config_with_token_provider();
+
+ let request: ApiRequest = ApiRequest::get(format!(
+ "/open-apis/bitable/v1/apps/{}/tables/{}/records",
+ coordination.base_token, coordination.heartbeat_table_id
+ ))
+ .query("page_size", "10");
+ let response = Transport::::request(request, &config, Some(Default::default()))
+ .await
+ .context("request failed")?;
+ println!(
+ "{}",
+ serde_json::to_string_pretty(&serde_json::json!({
+ "success": response.is_success(),
+ "msg": response.msg(),
+ "data": response.data,
+ }))?
+ );
+ Ok::<(), anyhow::Error>(())
+ })?;
+ Ok(())
+}
+
+fn expand_home(path: PathBuf) -> PathBuf {
+ if let Some(text) = path.to_str()
+ && let Some(stripped) = text.strip_prefix("~/")
+ && let Some(home) = std::env::var_os("HOME")
+ {
+ return PathBuf::from(home).join(stripped);
+ }
+ path
+}
diff --git a/codex-rs/clawbot/src/bin/feishu-payload-dump.rs b/codex-rs/clawbot/src/bin/feishu-payload-dump.rs
new file mode 100644
index 000000000..b981256d7
--- /dev/null
+++ b/codex-rs/clawbot/src/bin/feishu-payload-dump.rs
@@ -0,0 +1,174 @@
+use std::env;
+use std::fs;
+use std::fs::OpenOptions;
+use std::io::Write;
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::time::Duration;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use anyhow::Context;
+use anyhow::Result;
+use anyhow::anyhow;
+use codex_clawbot::CLAWBOT_RELATIVE_DIR;
+use codex_clawbot::ClawbotRuntime;
+use codex_clawbot::FeishuConfig;
+use open_lark::openlark_client;
+use open_lark::openlark_client::ws_client::EventDispatcherHandler;
+use open_lark::openlark_client::ws_client::LarkWsClient;
+use serde_json::Value;
+use tokio::runtime::Builder;
+use tokio::sync::mpsc;
+
+const INITIAL_RECONNECT_DELAY: Duration = Duration::from_secs(2);
+const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30);
+const DEFAULT_DUMP_FILE_NAME: &str = "feishu_payload_dump.jsonl";
+
+fn main() -> Result<()> {
+ Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .context("failed to build tokio runtime")?
+ .block_on(async_main())
+}
+
+async fn async_main() -> Result<()> {
+ let workspace_root = workspace_root_from_args()?;
+ let runtime = ClawbotRuntime::load(workspace_root.clone()).with_context(|| {
+ format!(
+ "failed to load clawbot config from {}",
+ workspace_root.display()
+ )
+ })?;
+ let feishu = runtime
+ .snapshot()
+ .config
+ .feishu
+ .clone()
+ .context("missing [feishu] config in .codex/clawbot/config.toml")?;
+ if !feishu.has_api_credentials() {
+ return Err(anyhow!(
+ "missing app_id/app_secret in clawbot feishu config"
+ ));
+ }
+
+ let dump_path = dump_path_from_args(&workspace_root);
+ ensure_parent_dir(&dump_path)?;
+ eprintln!("workspace: {}", workspace_root.display());
+ eprintln!("dump file: {}", dump_path.display());
+
+ let mut reconnect_delay = INITIAL_RECONNECT_DELAY;
+ loop {
+ match run_once(&feishu, &dump_path).await {
+ Ok(()) => {
+ eprintln!(
+ "feishu websocket exited; reconnecting in {}s",
+ reconnect_delay.as_secs()
+ );
+ }
+ Err(err) => {
+ eprintln!(
+ "feishu websocket failed: {err}; reconnecting in {}s",
+ reconnect_delay.as_secs()
+ );
+ }
+ }
+
+ tokio::time::sleep(reconnect_delay).await;
+ reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY);
+ }
+}
+
+async fn run_once(feishu: &FeishuConfig, dump_path: &Path) -> Result<()> {
+ let ws_config = Arc::new(build_websocket_config(feishu)?);
+ let (payload_tx, mut payload_rx) = mpsc::unbounded_channel::>();
+ let dump_path = dump_path.to_path_buf();
+ let payload_task = tokio::spawn(async move {
+ while let Some(payload) = payload_rx.recv().await {
+ if let Err(err) = append_payload_record(&dump_path, &payload) {
+ eprintln!("failed to write payload record: {err}");
+ }
+ }
+ });
+
+ let event_handler = EventDispatcherHandler::builder()
+ .payload_sender(payload_tx)
+ .build();
+ eprintln!("feishu websocket connected; waiting for events...");
+ let result = LarkWsClient::open(ws_config, event_handler)
+ .await
+ .map_err(|err| anyhow!("websocket runtime failed: {err}"));
+ payload_task.abort();
+ let _ = payload_task.await;
+ result
+}
+
+fn workspace_root_from_args() -> Result {
+ Ok(match env::args().nth(1) {
+ Some(path) => PathBuf::from(path),
+ None => env::current_dir().context("failed to resolve current directory")?,
+ })
+}
+
+fn dump_path_from_args(workspace_root: &Path) -> PathBuf {
+ env::args().nth(2).map_or_else(
+ || {
+ workspace_root
+ .join(CLAWBOT_RELATIVE_DIR)
+ .join(DEFAULT_DUMP_FILE_NAME)
+ },
+ PathBuf::from,
+ )
+}
+
+fn ensure_parent_dir(path: &Path) -> Result<()> {
+ let parent = path
+ .parent()
+ .ok_or_else(|| anyhow!("path has no parent: {}", path.display()))?;
+ fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))
+}
+
+fn build_websocket_config(feishu: &FeishuConfig) -> Result {
+ openlark_client::Config::builder()
+ .app_id(feishu.app_id.clone())
+ .app_secret(feishu.app_secret.clone())
+ .timeout(Duration::from_secs(30))
+ .build()
+ .map_err(|err| anyhow!("failed to build websocket config: {err}"))
+}
+
+fn append_payload_record(dump_path: &Path, payload: &[u8]) -> Result<()> {
+ let raw_text = String::from_utf8_lossy(payload).into_owned();
+ let parsed_payload = serde_json::from_slice::(payload).ok();
+ let event_type = parsed_payload
+ .as_ref()
+ .and_then(|value| value.pointer("/header/event_type"))
+ .and_then(Value::as_str)
+ .map(ToOwned::to_owned);
+ let recorded_at = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .context("system clock is before UNIX_EPOCH")?
+ .as_secs() as i64;
+ let record = match parsed_payload {
+ Some(payload) => serde_json::json!({
+ "recorded_at": recorded_at,
+ "event_type": event_type,
+ "payload": payload,
+ }),
+ None => serde_json::json!({
+ "recorded_at": recorded_at,
+ "event_type": event_type,
+ "payload_text": raw_text,
+ }),
+ };
+
+ let mut file = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(dump_path)
+ .with_context(|| format!("failed to open {}", dump_path.display()))?;
+ writeln!(file, "{}", serde_json::to_string(&record)?)
+ .with_context(|| format!("failed to append {}", dump_path.display()))
+}
diff --git a/codex-rs/clawbot/src/bin/feishu-runtime-probe.rs b/codex-rs/clawbot/src/bin/feishu-runtime-probe.rs
new file mode 100644
index 000000000..9f9e91747
--- /dev/null
+++ b/codex-rs/clawbot/src/bin/feishu-runtime-probe.rs
@@ -0,0 +1,122 @@
+use std::env;
+use std::path::PathBuf;
+use std::time::Duration;
+
+use anyhow::Context;
+use anyhow::Result;
+use codex_clawbot::ClawbotRuntime;
+use codex_clawbot::FeishuProviderRuntime;
+use codex_clawbot::ProviderEvent;
+use tokio::runtime::Builder;
+use tokio::sync::mpsc;
+
+fn main() -> Result<()> {
+ Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .context("failed to build tokio runtime")?
+ .block_on(async_main())
+}
+
+async fn async_main() -> Result<()> {
+ let (workspace_root, timeout_secs) = parse_args()?;
+ let runtime = ClawbotRuntime::load(workspace_root.clone())
+ .with_context(|| format!("failed to load workspace {}", workspace_root.display()))?;
+ let provider = runtime
+ .feishu_provider()
+ .context("missing [feishu] config in workspace clawbot config")?;
+
+ eprintln!(
+ "probe starting: workspace={} timeout_secs={timeout_secs}",
+ workspace_root.display()
+ );
+
+ let (provider_event_tx, mut provider_event_rx) = mpsc::unbounded_channel();
+ let provider_task = tokio::spawn(run_provider(provider, provider_event_tx));
+ let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
+
+ loop {
+ tokio::select! {
+ _ = tokio::time::sleep_until(deadline) => {
+ eprintln!("probe timeout reached");
+ break;
+ }
+ maybe_event = provider_event_rx.recv() => {
+ let Some(event) = maybe_event else {
+ eprintln!("provider event channel closed");
+ break;
+ };
+ print_event(event);
+ }
+ }
+ }
+
+ provider_task.abort();
+ let _ = provider_task.await;
+ Ok(())
+}
+
+async fn run_provider(
+ provider: FeishuProviderRuntime,
+ provider_event_tx: mpsc::UnboundedSender,
+) {
+ if let Err(error) = provider.run(provider_event_tx).await {
+ eprintln!("provider exited with error: {error}");
+ }
+}
+
+fn print_event(event: ProviderEvent) {
+ match event {
+ ProviderEvent::RuntimeStateUpdated(state) => {
+ println!(
+ "{{\"type\":\"runtime_state\",\"connection\":\"{:?}\",\"last_error\":{},\"updated_at\":{}}}",
+ state.connection,
+ serde_json::to_string(&state.last_error).unwrap_or_else(|_| "null".to_string()),
+ state
+ .updated_at
+ .map(|value| value.to_string())
+ .unwrap_or_else(|| "null".to_string()),
+ );
+ }
+ ProviderEvent::SessionUpserted(session) => {
+ println!(
+ "{{\"type\":\"session_upserted\",\"session_id\":{},\"status\":\"{:?}\"}}",
+ serde_json::to_string(&session.session_id)
+ .unwrap_or_else(|_| "\"\"".to_string()),
+ session.status,
+ );
+ }
+ ProviderEvent::SessionRemoved(session) => {
+ println!(
+ "{{\"type\":\"session_removed\",\"session_id\":{}}}",
+ serde_json::to_string(&session.session_id)
+ .unwrap_or_else(|_| "\"\"".to_string()),
+ );
+ }
+ ProviderEvent::InboundMessage(message) => {
+ println!(
+ "{{\"type\":\"inbound_message\",\"session_id\":{},\"message_id\":{}}}",
+ serde_json::to_string(&message.session.session_id)
+ .unwrap_or_else(|_| "\"\"".to_string()),
+ serde_json::to_string(&message.message_id)
+ .unwrap_or_else(|_| "\"\"".to_string()),
+ );
+ }
+ }
+}
+
+fn parse_args() -> Result<(PathBuf, u64)> {
+ let mut args = env::args().skip(1);
+ let workspace_root = args
+ .next()
+ .map(PathBuf::from)
+ .unwrap_or(env::current_dir().context("failed to resolve current directory")?);
+ let timeout_secs = args
+ .next()
+ .as_deref()
+ .map(str::parse)
+ .transpose()
+ .context("failed to parse timeout seconds")?
+ .unwrap_or(60);
+ Ok((workspace_root, timeout_secs))
+}
diff --git a/codex-rs/clawbot/src/config.rs b/codex-rs/clawbot/src/config.rs
new file mode 100644
index 000000000..a59465411
--- /dev/null
+++ b/codex-rs/clawbot/src/config.rs
@@ -0,0 +1,174 @@
+use serde::Deserialize;
+use serde::Serialize;
+use std::time::Duration;
+
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ClawbotTurnMode {
+ #[default]
+ Interactive,
+ NonInteractive,
+}
+
+impl ClawbotTurnMode {
+ pub fn uses_noninteractive_prompt_handling(self) -> bool {
+ matches!(self, Self::NonInteractive)
+ }
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(default)]
+pub struct ClawbotConfig {
+ pub feishu: Option,
+ pub turn_mode: ClawbotTurnMode,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(default)]
+pub struct FeishuConfig {
+ pub app_id: String,
+ pub app_secret: String,
+ pub verification_token: Option,
+ pub encrypt_key: Option,
+ pub bot_open_id: Option,
+ pub bot_user_id: Option,
+ pub coordination: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(default)]
+pub struct FeishuCoordinationConfig {
+ pub base_token: String,
+ pub heartbeat_table_id: String,
+ pub force_table_id: String,
+ pub instance_id: Option,
+ pub owner_priority: i64,
+ pub heartbeat_interval_secs: u64,
+ pub heartbeat_ttl_secs: u64,
+ pub force_connect: bool,
+}
+
+impl Default for FeishuCoordinationConfig {
+ fn default() -> Self {
+ Self {
+ base_token: String::new(),
+ heartbeat_table_id: String::new(),
+ force_table_id: String::new(),
+ instance_id: None,
+ owner_priority: 100,
+ heartbeat_interval_secs: 10,
+ heartbeat_ttl_secs: 30,
+ force_connect: false,
+ }
+ }
+}
+
+impl FeishuCoordinationConfig {
+ pub fn is_configured(&self) -> bool {
+ !self.base_token.trim().is_empty()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.base_token.trim().is_empty()
+ && self.heartbeat_table_id.trim().is_empty()
+ && self.force_table_id.trim().is_empty()
+ && self
+ .instance_id
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ && self.owner_priority == Self::default().owner_priority
+ && self.heartbeat_interval_secs == Self::default().heartbeat_interval_secs
+ && self.heartbeat_ttl_secs == Self::default().heartbeat_ttl_secs
+ && !self.force_connect
+ }
+
+ pub fn heartbeat_interval(&self) -> Duration {
+ Duration::from_secs(self.heartbeat_interval_secs.max(1))
+ }
+
+ pub fn heartbeat_ttl(&self) -> Duration {
+ Duration::from_secs(
+ self.heartbeat_ttl_secs
+ .max(self.heartbeat_interval_secs.max(1) * 2),
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::FeishuCoordinationConfig;
+
+ #[test]
+ fn coordination_is_configured_with_base_token_only() {
+ let mut config = FeishuCoordinationConfig {
+ base_token: "bascn_test".to_string(),
+ ..FeishuCoordinationConfig::default()
+ };
+
+ assert!(config.is_configured());
+
+ config.base_token.clear();
+ assert!(!config.is_configured());
+ }
+
+ #[test]
+ fn coordination_ttl_is_at_least_twice_the_interval() {
+ let config = FeishuCoordinationConfig {
+ base_token: "bascn_test".to_string(),
+ heartbeat_interval_secs: 9,
+ heartbeat_ttl_secs: 5,
+ ..FeishuCoordinationConfig::default()
+ };
+
+ assert_eq!(config.heartbeat_ttl().as_secs(), 18);
+ }
+}
+
+impl FeishuConfig {
+ pub fn has_api_credentials(&self) -> bool {
+ !self.app_id.trim().is_empty() && !self.app_secret.trim().is_empty()
+ }
+
+ pub fn is_bot_sender(
+ &self,
+ open_id: Option<&str>,
+ user_id: Option<&str>,
+ app_id: Option<&str>,
+ ) -> bool {
+ self.bot_open_id
+ .as_deref()
+ .zip(open_id)
+ .is_some_and(|(bot_open_id, sender_open_id)| bot_open_id == sender_open_id)
+ || self
+ .bot_user_id
+ .as_deref()
+ .zip(user_id)
+ .is_some_and(|(bot_user_id, sender_user_id)| bot_user_id == sender_user_id)
+ || app_id.is_some_and(|sender_app_id| sender_app_id == self.app_id)
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.app_id.trim().is_empty()
+ && self.app_secret.trim().is_empty()
+ && self
+ .verification_token
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ && self
+ .encrypt_key
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ && self
+ .bot_open_id
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ && self
+ .bot_user_id
+ .as_deref()
+ .is_none_or(|value| value.trim().is_empty())
+ && self
+ .coordination
+ .as_ref()
+ .is_none_or(FeishuCoordinationConfig::is_empty)
+ }
+}
diff --git a/codex-rs/clawbot/src/diagnostics.rs b/codex-rs/clawbot/src/diagnostics.rs
new file mode 100644
index 000000000..bd32bfe1d
--- /dev/null
+++ b/codex-rs/clawbot/src/diagnostics.rs
@@ -0,0 +1,50 @@
+use std::fs::OpenOptions;
+use std::io::Write;
+use std::path::Path;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use anyhow::Context;
+use anyhow::Result;
+use serde::Serialize;
+
+use crate::model::CLAWBOT_DIAGNOSTICS_RELATIVE_PATH;
+
+#[derive(Debug, Serialize)]
+struct DiagnosticEvent {
+ ts_ms: i64,
+ kind: String,
+ payload: T,
+}
+
+pub fn append_diagnostic_event(workspace_root: &Path, kind: &str, payload: T) -> Result<()>
+where
+ T: Serialize,
+{
+ let path = workspace_root.join(CLAWBOT_DIAGNOSTICS_RELATIVE_PATH);
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent)
+ .with_context(|| format!("failed to create {}", parent.display()))?;
+ }
+ let event = DiagnosticEvent {
+ ts_ms: unix_timestamp_ms_now()?,
+ kind: kind.to_string(),
+ payload,
+ };
+ let mut file = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(&path)
+ .with_context(|| format!("failed to open {}", path.display()))?;
+ serde_json::to_writer(&mut file, &event)
+ .context("failed to encode clawbot diagnostic event")?;
+ file.write_all(b"\n")
+ .with_context(|| format!("failed to append {}", path.display()))
+}
+
+fn unix_timestamp_ms_now() -> Result {
+ Ok(SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .context("system clock is before unix epoch")?
+ .as_millis() as i64)
+}
diff --git a/codex-rs/clawbot/src/events.rs b/codex-rs/clawbot/src/events.rs
new file mode 100644
index 000000000..d66710856
--- /dev/null
+++ b/codex-rs/clawbot/src/events.rs
@@ -0,0 +1,12 @@
+use serde::Deserialize;
+use serde::Serialize;
+
+use crate::model::ProviderSessionRef;
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ProviderInboundMessage {
+ pub session: ProviderSessionRef,
+ pub message_id: String,
+ pub text: String,
+ pub received_at: i64,
+}
diff --git a/codex-rs/clawbot/src/lib.rs b/codex-rs/clawbot/src/lib.rs
new file mode 100644
index 000000000..250e49844
--- /dev/null
+++ b/codex-rs/clawbot/src/lib.rs
@@ -0,0 +1,43 @@
+mod config;
+mod diagnostics;
+mod events;
+mod model;
+mod provider;
+mod runtime;
+mod store;
+
+pub use config::ClawbotConfig;
+pub use config::ClawbotTurnMode;
+pub use config::FeishuConfig;
+pub use config::FeishuCoordinationConfig;
+pub use diagnostics::append_diagnostic_event;
+pub use events::ProviderInboundMessage;
+pub use model::CLAWBOT_BINDINGS_RELATIVE_PATH;
+pub use model::CLAWBOT_CONFIG_RELATIVE_PATH;
+pub use model::CLAWBOT_DIAGNOSTICS_RELATIVE_PATH;
+pub use model::CLAWBOT_INBOUND_RECEIPTS_RELATIVE_PATH;
+pub use model::CLAWBOT_PENDING_TURNS_RELATIVE_PATH;
+pub use model::CLAWBOT_RELATIVE_DIR;
+pub use model::CLAWBOT_RUNTIME_RELATIVE_PATH;
+pub use model::CLAWBOT_SESSIONS_RELATIVE_PATH;
+pub use model::CLAWBOT_UNREAD_MESSAGES_RELATIVE_PATH;
+pub use model::CachedUnreadMessage;
+pub use model::ClawbotSnapshot;
+pub use model::ConnectionStatus;
+pub use model::ForwardingDirection;
+pub use model::ForwardingState;
+pub use model::PendingClawbotTurn;
+pub use model::ProviderKind;
+pub use model::ProviderMessageRef;
+pub use model::ProviderRuntimeState;
+pub use model::ProviderSession;
+pub use model::ProviderSessionRef;
+pub use model::SessionBinding;
+pub use model::SessionStatus;
+pub use provider::FeishuProviderRuntime;
+pub use provider::ProviderEvent;
+pub use provider::ProviderOutboundReaction;
+pub use provider::ProviderOutboundTextMessage;
+pub use provider::feishu_failure_reply_text;
+pub use runtime::ClawbotRuntime;
+pub use store::ClawbotStore;
diff --git a/codex-rs/clawbot/src/model.rs b/codex-rs/clawbot/src/model.rs
new file mode 100644
index 000000000..82a4f18f1
--- /dev/null
+++ b/codex-rs/clawbot/src/model.rs
@@ -0,0 +1,209 @@
+use serde::Deserialize;
+use serde::Serialize;
+
+use crate::config::ClawbotConfig;
+use crate::config::ClawbotTurnMode;
+
+pub const CLAWBOT_RELATIVE_DIR: &str = ".codex/clawbot";
+pub const CLAWBOT_CONFIG_RELATIVE_PATH: &str = ".codex/clawbot/config.toml";
+pub const CLAWBOT_SESSIONS_RELATIVE_PATH: &str = ".codex/clawbot/sessions.json";
+pub const CLAWBOT_BINDINGS_RELATIVE_PATH: &str = ".codex/clawbot/bindings.json";
+pub const CLAWBOT_UNREAD_MESSAGES_RELATIVE_PATH: &str = ".codex/clawbot/unread_messages.jsonl";
+pub const CLAWBOT_PENDING_TURNS_RELATIVE_PATH: &str = ".codex/clawbot/pending_turns.json";
+pub const CLAWBOT_RUNTIME_RELATIVE_PATH: &str = ".codex/clawbot/runtime.json";
+pub const CLAWBOT_INBOUND_RECEIPTS_RELATIVE_PATH: &str = ".codex/clawbot/inbound_receipts.json";
+pub const CLAWBOT_DIAGNOSTICS_RELATIVE_PATH: &str = ".codex/clawbot/diagnostics.jsonl";
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub struct ClawbotSnapshot {
+ pub config: ClawbotConfig,
+ pub runtime: Vec,
+ pub sessions: Vec,
+ pub bindings: Vec,
+ pub unread_message_count: usize,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
+#[serde(rename_all = "snake_case")]
+pub enum ProviderKind {
+ Feishu,
+}
+
+impl ProviderKind {
+ pub fn title(self) -> &'static str {
+ match self {
+ Self::Feishu => "Feishu",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ConnectionStatus {
+ #[default]
+ Unconfigured,
+ Disconnected,
+ Connecting,
+ Connected,
+ Error,
+}
+
+#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum SessionStatus {
+ #[default]
+ Discovered,
+ Bound,
+ Disconnected,
+ Error,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ProviderRuntimeState {
+ pub provider: ProviderKind,
+ pub connection: ConnectionStatus,
+ pub last_error: Option,
+ pub updated_at: Option,
+}
+
+impl ProviderRuntimeState {
+ pub fn unconfigured(provider: ProviderKind) -> Self {
+ Self {
+ provider,
+ connection: ConnectionStatus::Unconfigured,
+ last_error: None,
+ updated_at: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct ProviderSessionRef {
+ pub provider: ProviderKind,
+ pub session_id: String,
+}
+
+impl ProviderSessionRef {
+ pub fn new(provider: ProviderKind, session_id: impl Into) -> Self {
+ Self {
+ provider,
+ session_id: session_id.into(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct ProviderMessageRef {
+ pub provider: ProviderKind,
+ pub session_id: String,
+ pub message_id: String,
+}
+
+impl ProviderMessageRef {
+ pub fn new(
+ provider: ProviderKind,
+ session_id: impl Into,
+ message_id: impl Into,
+ ) -> Self {
+ Self {
+ provider,
+ session_id: session_id.into(),
+ message_id: message_id.into(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct ProviderSession {
+ pub provider: ProviderKind,
+ pub session_id: String,
+ pub display_name: Option,
+ pub unread_count: usize,
+ pub last_message_at: Option,
+ pub status: SessionStatus,
+ pub bound_thread_id: Option,
+}
+
+impl ProviderSession {
+ pub fn session_ref(&self) -> ProviderSessionRef {
+ ProviderSessionRef::new(self.provider, self.session_id.clone())
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct SessionBinding {
+ pub provider: ProviderKind,
+ pub session_id: String,
+ pub thread_id: String,
+ #[serde(default)]
+ pub owner_primary_thread_id: Option,
+ #[serde(default = "default_session_forwarding_enabled")]
+ pub inbound_forwarding_enabled: bool,
+ #[serde(default = "default_session_forwarding_enabled")]
+ pub outbound_forwarding_enabled: bool,
+ pub created_at: i64,
+ pub updated_at: i64,
+}
+
+impl SessionBinding {
+ pub fn session_ref(&self) -> ProviderSessionRef {
+ ProviderSessionRef::new(self.provider, self.session_id.clone())
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ForwardingDirection {
+ Inbound,
+ Outbound,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ForwardingState {
+ Enabled,
+ Disabled,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct CachedUnreadMessage {
+ pub provider: ProviderKind,
+ pub session_id: String,
+ pub message_id: String,
+ pub text: String,
+ pub received_at: i64,
+}
+
+impl CachedUnreadMessage {
+ pub fn session_ref(&self) -> ProviderSessionRef {
+ ProviderSessionRef::new(self.provider, self.session_id.clone())
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct PendingClawbotTurn {
+ pub thread_id: String,
+ pub turn_id: String,
+ #[serde(default)]
+ pub owner_primary_thread_id: Option,
+ pub session: ProviderSessionRef,
+ pub message_id: String,
+ pub auto_ack_reaction_id: Option,
+ pub turn_mode: ClawbotTurnMode,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct InboundMessageReceipt {
+ pub provider: ProviderKind,
+ pub session_id: String,
+ pub message_id: String,
+ pub received_at: i64,
+}
+
+impl InboundMessageReceipt {
+ pub fn session_ref(&self) -> ProviderSessionRef {
+ ProviderSessionRef::new(self.provider, self.session_id.clone())
+ }
+}
+
+fn default_session_forwarding_enabled() -> bool {
+ true
+}
diff --git a/codex-rs/clawbot/src/provider/feishu.rs b/codex-rs/clawbot/src/provider/feishu.rs
new file mode 100644
index 000000000..030f81f34
--- /dev/null
+++ b/codex-rs/clawbot/src/provider/feishu.rs
@@ -0,0 +1,1122 @@
+mod coordination;
+mod runtime_loop;
+mod sync;
+
+use std::path::Path;
+use std::path::PathBuf;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use anyhow::Result;
+use anyhow::anyhow;
+use open_lark::openlark_client;
+use open_lark::openlark_communication::common::api_utils::serialize_params;
+use open_lark::openlark_communication::endpoints::IM_V1_MESSAGES;
+use open_lark::openlark_communication::im::im::v1::message::create::CreateMessageBody;
+use open_lark::openlark_communication::im::im::v1::message::create::CreateMessageRequest;
+use open_lark::openlark_communication::im::im::v1::message::models::ReceiveIdType;
+use open_lark::openlark_communication::im::im::v1::message::models::UserIdType;
+use open_lark::openlark_communication::im::im::v1::message::reaction::list::ListMessageReactionsRequest;
+use open_lark::openlark_communication::im::im::v1::message::reaction::models::CreateMessageReactionBody;
+use open_lark::openlark_communication::im::im::v1::message::reaction::models::MessageReaction;
+use open_lark::openlark_communication::im::im::v1::message::reaction::models::ReactionType;
+use open_lark::openlark_core::api::ApiRequest;
+use serde::Deserialize;
+use serde_json::Value;
+use tokio::sync::mpsc;
+
+use super::ProviderEvent;
+use super::ProviderOutboundReaction;
+use super::ProviderOutboundTextMessage;
+use crate::append_diagnostic_event;
+use crate::config::FeishuConfig;
+use crate::events::ProviderInboundMessage;
+use crate::model::ConnectionStatus;
+use crate::model::ProviderKind;
+use crate::model::ProviderRuntimeState;
+use crate::model::ProviderSession;
+use crate::model::ProviderSessionRef;
+use crate::model::SessionStatus;
+
+#[derive(Debug, Clone)]
+pub struct FeishuInboundMessage {
+ pub chat_id: String,
+ pub chat_type: String,
+ pub chat_name: Option,
+ pub message_id: String,
+ pub sender_open_id: Option,
+ pub sender_user_id: Option,
+ pub sender_union_id: Option,
+ pub text: String,
+ pub received_at: i64,
+}
+
+#[derive(Debug, Clone)]
+pub struct FeishuProviderRuntime {
+ workspace_root: PathBuf,
+ config: FeishuConfig,
+}
+
+impl FeishuProviderRuntime {
+ pub fn new(workspace_root: impl Into, config: FeishuConfig) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ config,
+ }
+ }
+
+ pub async fn run(self, provider_event_tx: mpsc::UnboundedSender) -> Result<()> {
+ runtime_loop::run_with_reconnect(self.workspace_root, self.config, provider_event_tx).await
+ }
+
+ pub async fn send_text(&self, message: ProviderOutboundTextMessage) -> Result<()> {
+ if message.session.provider != ProviderKind::Feishu {
+ return Err(anyhow!(
+ "cannot send {} message via Feishu runtime",
+ message.session.provider.title()
+ ));
+ }
+
+ let text = message.text;
+ let session_id = message.session.session_id;
+ let response = CreateMessageRequest::new(self.messaging_config()?)
+ .receive_id_type(ReceiveIdType::ChatId)
+ .execute(CreateMessageBody {
+ receive_id: session_id.clone(),
+ msg_type: "text".to_string(),
+ content: serde_json::to_string(&serde_json::json!({ "text": text.clone() }))?,
+ uuid: None,
+ })
+ .await
+ .map_err(|error| anyhow!("failed to send Feishu text message: {error}"));
+ match response {
+ Ok(_) => {
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.send_text_succeeded",
+ serde_json::json!({
+ "session_id": session_id,
+ "text": text,
+ }),
+ );
+ Ok(())
+ }
+ Err(error) => {
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.send_text_failed",
+ serde_json::json!({
+ "session_id": session_id,
+ "text": text,
+ "error": error.to_string(),
+ }),
+ );
+ Err(error)
+ }
+ }
+ }
+
+ pub async fn add_reaction(&self, reaction: ProviderOutboundReaction) -> Result> {
+ if reaction.target.provider != ProviderKind::Feishu {
+ return Err(anyhow!(
+ "cannot send {} reaction via Feishu runtime",
+ reaction.target.provider.title()
+ ));
+ }
+
+ let session_id = reaction.target.session_id.clone();
+ let message_id = reaction.target.message_id.clone();
+ let emoji_type = reaction.emoji_type.clone();
+ let request: ApiRequest = ApiRequest::post(format!(
+ "{IM_V1_MESSAGES}/{}/reactions",
+ reaction.target.message_id
+ ))
+ .body(serialize_params(
+ &CreateMessageReactionBody {
+ reaction_type: ReactionType {
+ emoji_type: reaction.emoji_type,
+ },
+ },
+ "添加消息表情回复",
+ )?);
+ let response = open_lark::openlark_core::http::Transport::::request(
+ request,
+ &self.messaging_config()?,
+ Some(Default::default()),
+ )
+ .await
+ .map_err(|error| anyhow!("failed to add Feishu message reaction: {error}"))?;
+ if response.is_success() {
+ let reaction_id = response
+ .data
+ .as_ref()
+ .and_then(|data| data.get("reaction_id"))
+ .and_then(Value::as_str)
+ .map(str::to_owned);
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.add_reaction_succeeded",
+ serde_json::json!({
+ "session_id": session_id,
+ "message_id": message_id,
+ "emoji_type": emoji_type,
+ "reaction_id": reaction_id,
+ }),
+ );
+ Ok(reaction_id)
+ } else {
+ let error = anyhow!("failed to add Feishu message reaction: {}", response.msg());
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.add_reaction_failed",
+ serde_json::json!({
+ "session_id": session_id,
+ "message_id": message_id,
+ "emoji_type": emoji_type,
+ "error": error.to_string(),
+ }),
+ );
+ Err(error)
+ }
+ }
+
+ pub async fn remove_reaction(&self, reaction: ProviderOutboundReaction) -> Result<()> {
+ if reaction.target.provider != ProviderKind::Feishu {
+ return Err(anyhow!(
+ "cannot send {} reaction via Feishu runtime",
+ reaction.target.provider.title()
+ ));
+ }
+
+ let config = self.messaging_config()?;
+ let matching_reactions = self
+ .list_message_reactions(
+ &config,
+ reaction.target.message_id.as_str(),
+ reaction.emoji_type.as_str(),
+ )
+ .await?;
+ let reaction_ids = self.select_reaction_ids_to_remove(&reaction, &matching_reactions);
+
+ if reaction_ids.is_empty() {
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.remove_reaction_skipped",
+ serde_json::json!({
+ "session_id": reaction.target.session_id,
+ "message_id": reaction.target.message_id,
+ "emoji_type": reaction.emoji_type,
+ "bot_open_id": self.config.bot_open_id,
+ "bot_user_id": self.config.bot_user_id,
+ "matching_reaction_count": matching_reactions.len(),
+ "matching_reactions": matching_reactions
+ .iter()
+ .map(|item| serde_json::json!({
+ "reaction_id": item.reaction_id,
+ "operator_id": item.operator.operator_id,
+ "operator_type": item.operator.operator_type,
+ "emoji_type": item.reaction_type.emoji_type,
+ }))
+ .collect::>(),
+ }),
+ );
+ return Ok(());
+ }
+
+ for reaction_id in &reaction_ids {
+ self.remove_reaction_by_id(reaction.clone(), reaction_id)
+ .await?;
+ }
+
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.remove_reaction_succeeded",
+ serde_json::json!({
+ "session_id": reaction.target.session_id,
+ "message_id": reaction.target.message_id,
+ "emoji_type": reaction.emoji_type,
+ "removed_count": reaction_ids.len(),
+ "remove_mode": "listed",
+ }),
+ );
+ Ok(())
+ }
+
+ pub async fn remove_reaction_by_id(
+ &self,
+ reaction: ProviderOutboundReaction,
+ reaction_id: &str,
+ ) -> Result<()> {
+ if reaction.target.provider != ProviderKind::Feishu {
+ return Err(anyhow!(
+ "cannot send {} reaction via Feishu runtime",
+ reaction.target.provider.title()
+ ));
+ }
+
+ let request: ApiRequest = ApiRequest::delete(format!(
+ "{}/{}/reactions/{}",
+ IM_V1_MESSAGES, reaction.target.message_id, reaction_id
+ ));
+ let response = open_lark::openlark_core::http::Transport::::request(
+ request,
+ &self.messaging_config()?,
+ Some(Default::default()),
+ )
+ .await
+ .map_err(|error| anyhow!("failed to remove Feishu message reaction: {error}"))?;
+ if !response.is_success() {
+ return Err(anyhow!(
+ "failed to remove Feishu message reaction: {}",
+ response.msg()
+ ));
+ }
+
+ let _ = append_diagnostic_event(
+ self.workspace_root.as_path(),
+ "feishu.remove_reaction_succeeded",
+ serde_json::json!({
+ "session_id": reaction.target.session_id,
+ "message_id": reaction.target.message_id,
+ "emoji_type": reaction.emoji_type,
+ "removed_count": 1,
+ "reaction_id": reaction_id,
+ "remove_mode": "direct",
+ }),
+ );
+ Ok(())
+ }
+
+ pub async fn scan_sessions(&self) -> Result> {
+ let sessions = sync::discover_supported_sessions(&self.messaging_config()?).await?;
+ Ok(sessions
+ .into_iter()
+ .map(ProviderEvent::SessionUpserted)
+ .collect())
+ }
+
+ pub fn normalize_chat_message(message: FeishuInboundMessage) -> Option> {
+ if !is_supported_chat_type(&message.chat_type) || message.text.trim().is_empty() {
+ return None;
+ }
+
+ let display_name = if is_group_chat_type(&message.chat_type) {
+ message.chat_name
+ } else {
+ message
+ .chat_name
+ .or(message.sender_open_id.clone())
+ .or(message.sender_user_id.clone())
+ .or(message.sender_union_id.clone())
+ };
+ let session = ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: message.chat_id.clone(),
+ display_name,
+ unread_count: 0,
+ last_message_at: Some(message.received_at),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ };
+ let inbound_message = ProviderInboundMessage {
+ session: ProviderSessionRef::new(ProviderKind::Feishu, message.chat_id),
+ message_id: message.message_id,
+ text: message.text,
+ received_at: message.received_at,
+ };
+
+ Some(vec![
+ ProviderEvent::SessionUpserted(session),
+ ProviderEvent::InboundMessage(inbound_message),
+ ])
+ }
+
+ pub(super) fn websocket_config(&self) -> Result {
+ runtime_loop::build_websocket_config(&self.config)
+ }
+
+ fn messaging_config(&self) -> Result {
+ Ok(self
+ .websocket_config()?
+ .build_core_config_with_token_provider())
+ }
+
+ async fn list_message_reactions(
+ &self,
+ config: &open_lark::openlark_core::config::Config,
+ message_id: &str,
+ emoji_type: &str,
+ ) -> Result> {
+ let mut page_token = None;
+ let mut reactions = Vec::new();
+
+ loop {
+ let mut request = ListMessageReactionsRequest::new(config.clone())
+ .message_id(message_id.to_string())
+ .reaction_type(emoji_type.to_string())
+ .page_size(50)
+ .user_id_type(UserIdType::OpenId);
+ if let Some(token) = page_token.clone() {
+ request = request.page_token(token);
+ }
+ let response = request
+ .execute()
+ .await
+ .map_err(|error| anyhow!("failed to list Feishu message reactions: {error}"))?;
+ reactions.extend(response.items.unwrap_or_default());
+ if !response.has_more {
+ break;
+ }
+ let Some(token) = response.page_token.filter(|token| !token.is_empty()) else {
+ break;
+ };
+ page_token = Some(token);
+ }
+
+ Ok(reactions)
+ }
+
+ fn select_reaction_ids_to_remove(
+ &self,
+ reaction: &ProviderOutboundReaction,
+ matching_reactions: &[MessageReaction],
+ ) -> Vec {
+ let bot_open_id = self.config.bot_open_id.as_deref();
+ let bot_user_id = self.config.bot_user_id.as_deref();
+ let exact_matches = matching_reactions
+ .iter()
+ .filter(|item| {
+ item.reaction_type.emoji_type == reaction.emoji_type
+ && (bot_open_id == Some(item.operator.operator_id.as_str())
+ || bot_user_id == Some(item.operator.operator_id.as_str()))
+ })
+ .map(|item| item.reaction_id.clone())
+ .collect::>();
+ if !exact_matches.is_empty() {
+ return exact_matches;
+ }
+
+ let fallback_matches = matching_reactions
+ .iter()
+ .filter(|item| item.reaction_type.emoji_type == reaction.emoji_type)
+ .map(|item| item.reaction_id.clone())
+ .collect::>();
+ if fallback_matches.len() == 1 {
+ return fallback_matches;
+ }
+
+ Vec::new()
+ }
+}
+
+pub fn failure_reply_text(message: &str) -> String {
+ let summary = message
+ .lines()
+ .map(str::trim)
+ .find(|line| !line.is_empty())
+ .unwrap_or("unknown error");
+ let truncated = truncate_chars(summary, /*max_chars*/ 160);
+ format!("Request failed: {truncated}")
+}
+
+pub(super) fn provider_events_from_payload(
+ payload: &[u8],
+ config: &FeishuConfig,
+ workspace_root: &Path,
+) -> Vec {
+ let Ok(envelope) = serde_json::from_slice::(payload) else {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.payload_parse_failed",
+ serde_json::json!({
+ "payload": String::from_utf8_lossy(payload),
+ }),
+ );
+ return Vec::new();
+ };
+
+ match envelope.header.event_type.as_str() {
+ "im.message.receive_v1" => {
+ serde_json::from_value::(envelope.event)
+ .ok()
+ .and_then(|event| {
+ normalize_message_receive_event(
+ FeishuMessageReceiveEnvelope { event },
+ config,
+ workspace_root,
+ )
+ })
+ .unwrap_or_default()
+ }
+ "im.chat.access_event.bot_p2p_chat_entered_v1" => {
+ serde_json::from_value::(envelope.event)
+ .ok()
+ .map(|event| normalize_chat_entered_event(FeishuChatEnteredEnvelope { event }))
+ .unwrap_or_default()
+ }
+ _ => {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.unsupported_event",
+ serde_json::json!({
+ "event_type": envelope.header.event_type,
+ }),
+ );
+ Vec::new()
+ }
+ }
+}
+
+fn normalize_message_receive_event(
+ envelope: FeishuMessageReceiveEnvelope,
+ config: &FeishuConfig,
+ workspace_root: &Path,
+) -> Option> {
+ let chat = envelope.event.chat;
+ let message = envelope.event.message;
+ let chat_type = message
+ .chat_type
+ .clone()
+ .or(chat.as_ref().and_then(|chat| chat.chat_type.clone()));
+ let message_type = message
+ .message_type
+ .as_deref()
+ .or(message.msg_type.as_deref());
+ let sender = envelope.event.sender.sender_id;
+ let Some(chat_type) = chat_type else {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_dropped",
+ serde_json::json!({
+ "reason": "missing_chat_type",
+ "message_id": message.message_id,
+ }),
+ );
+ return None;
+ };
+ let Some(message_type) = message_type else {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_dropped",
+ serde_json::json!({
+ "reason": "missing_message_type",
+ "chat_id": message.chat_id,
+ "message_id": message.message_id,
+ "chat_type": chat_type,
+ }),
+ );
+ return None;
+ };
+ if !is_supported_chat_type(&chat_type) {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_dropped",
+ serde_json::json!({
+ "reason": "unsupported_chat_type",
+ "chat_id": message.chat_id,
+ "message_id": message.message_id,
+ "chat_type": chat_type,
+ }),
+ );
+ return None;
+ }
+ if message_type != "text" {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_dropped",
+ serde_json::json!({
+ "reason": "unsupported_message_type",
+ "chat_id": message.chat_id,
+ "message_id": message.message_id,
+ "chat_type": chat_type,
+ "message_type": message_type,
+ }),
+ );
+ return None;
+ }
+ if config.is_bot_sender(
+ sender.open_id.as_deref(),
+ sender.user_id.as_deref(),
+ sender.app_id.as_deref(),
+ ) {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_dropped",
+ serde_json::json!({
+ "reason": "bot_sender",
+ "chat_id": message.chat_id,
+ "message_id": message.message_id,
+ "chat_type": chat_type,
+ "sender_open_id": sender.open_id,
+ "sender_user_id": sender.user_id,
+ "sender_app_id": sender.app_id,
+ }),
+ );
+ return None;
+ }
+
+ let chat_id = chat
+ .as_ref()
+ .map(|chat| chat.chat_id.clone())
+ .or(message.chat_id.clone())?;
+ let raw_content = message
+ .content
+ .or(message.body.and_then(|body| body.content))
+ .unwrap_or_default();
+ let text = serde_json::from_str::(&raw_content)
+ .ok()
+ .map(|content| content.text)
+ .unwrap_or(raw_content);
+ let normalized_text = if is_group_chat_type(&chat_type) {
+ strip_group_mention_prefix(&text)
+ } else {
+ text
+ };
+ let received_at = parse_timestamp_value(message.create_time)?;
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.message_normalized",
+ serde_json::json!({
+ "chat_id": chat_id.clone(),
+ "chat_type": chat_type.clone(),
+ "message_id": message.message_id.clone(),
+ "sender_open_id": sender.open_id.clone(),
+ "sender_user_id": sender.user_id.clone(),
+ "sender_app_id": sender.app_id.clone(),
+ "text": normalized_text.clone(),
+ }),
+ );
+
+ FeishuProviderRuntime::normalize_chat_message(FeishuInboundMessage {
+ chat_id,
+ chat_type,
+ chat_name: chat.and_then(|chat| chat.name),
+ message_id: message.message_id,
+ sender_open_id: sender.open_id,
+ sender_user_id: sender.user_id,
+ sender_union_id: sender.union_id,
+ text: normalized_text,
+ received_at,
+ })
+}
+
+fn normalize_chat_entered_event(envelope: FeishuChatEnteredEnvelope) -> Vec {
+ let operator = envelope.event.operator_id;
+ vec![ProviderEvent::SessionUpserted(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: envelope.event.chat_id,
+ display_name: operator
+ .open_id
+ .clone()
+ .or(operator.user_id.clone())
+ .or(operator.union_id),
+ unread_count: 0,
+ last_message_at: parse_optional_timestamp(envelope.event.last_message_create_time),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ })]
+}
+
+fn parse_optional_timestamp(timestamp: Option) -> Option {
+ timestamp.and_then(|value| value.parse::().ok())
+}
+
+fn parse_timestamp_value(timestamp: serde_json::Value) -> Option {
+ match timestamp {
+ serde_json::Value::String(value) => parse_optional_timestamp(Some(value)),
+ serde_json::Value::Number(value) => value.as_i64(),
+ _ => None,
+ }
+}
+
+fn is_supported_chat_type(chat_type: &str) -> bool {
+ is_private_chat_type(chat_type) || is_group_chat_type(chat_type)
+}
+
+fn is_private_chat_type(chat_type: &str) -> bool {
+ matches!(chat_type, "p2p" | "private")
+}
+
+fn is_group_chat_type(chat_type: &str) -> bool {
+ matches!(chat_type, "group")
+}
+
+fn strip_group_mention_prefix(text: &str) -> String {
+ let mut remaining = text.trim_start();
+ let mut stripped = false;
+
+ loop {
+ let Some(after_at) = remaining.strip_prefix('@') else {
+ break;
+ };
+ let mention_len = after_at
+ .char_indices()
+ .find_map(|(idx, ch)| {
+ (ch.is_whitespace() || matches!(ch, ':' | ':' | ',' | ',')).then_some(idx)
+ })
+ .unwrap_or(after_at.len());
+ if mention_len == 0 {
+ break;
+ }
+ remaining = &after_at[mention_len..];
+ remaining = remaining.trim_start_matches(|ch: char| {
+ ch.is_whitespace() || matches!(ch, ':' | ':' | ',' | ',')
+ });
+ stripped = true;
+ }
+
+ if stripped {
+ remaining.to_string()
+ } else {
+ text.to_string()
+ }
+}
+
+pub(super) fn runtime_state(
+ connection: ConnectionStatus,
+ last_error: Option,
+) -> Result {
+ Ok(ProviderRuntimeState {
+ provider: ProviderKind::Feishu,
+ connection,
+ last_error,
+ updated_at: Some(unix_timestamp_now()?),
+ })
+}
+
+fn unix_timestamp_now() -> Result {
+ Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64)
+}
+
+fn truncate_chars(value: &str, max_chars: usize) -> String {
+ let mut chars = value.chars();
+ let truncated: String = chars.by_ref().take(max_chars).collect();
+ if chars.next().is_some() {
+ format!("{truncated}…")
+ } else {
+ truncated
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventEnvelope {
+ header: FeishuEventHeader,
+ event: serde_json::Value,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventHeader {
+ event_type: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuMessageReceiveEnvelope {
+ event: FeishuMessageReceiveEvent,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuMessageReceiveEvent {
+ sender: FeishuEventSender,
+ message: FeishuEventMessage,
+ #[serde(default)]
+ chat: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventSender {
+ sender_id: FeishuUserId,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuUserId {
+ open_id: Option,
+ user_id: Option,
+ union_id: Option,
+ #[serde(default)]
+ app_id: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventMessage {
+ message_id: String,
+ create_time: serde_json::Value,
+ #[serde(default)]
+ chat_id: Option,
+ #[serde(default)]
+ chat_type: Option,
+ #[serde(default)]
+ message_type: Option,
+ #[serde(default)]
+ msg_type: Option,
+ #[serde(default)]
+ content: Option,
+ #[serde(default)]
+ body: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventChat {
+ chat_id: String,
+ #[serde(default)]
+ chat_type: Option,
+ #[serde(default)]
+ name: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuTextContent {
+ text: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuEventMessageBody {
+ content: Option,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuChatEnteredEnvelope {
+ event: FeishuChatEnteredEvent,
+}
+
+#[derive(Debug, Deserialize)]
+struct FeishuChatEnteredEvent {
+ chat_id: String,
+ operator_id: FeishuUserId,
+ last_message_create_time: Option,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::Path;
+
+ use pretty_assertions::assert_eq;
+
+ use super::FeishuInboundMessage;
+ use super::failure_reply_text;
+ use super::normalize_chat_entered_event;
+ use super::normalize_message_receive_event;
+ use super::strip_group_mention_prefix;
+ use crate::config::FeishuConfig;
+ use crate::model::ProviderKind;
+ use crate::model::ProviderSession;
+ use crate::model::ProviderSessionRef;
+ use crate::model::SessionStatus;
+ use crate::provider::ProviderEvent;
+
+ #[test]
+ fn normalize_chat_message_creates_session_and_inbound_events() {
+ let events = super::FeishuProviderRuntime::normalize_chat_message(FeishuInboundMessage {
+ chat_id: "chat_123".to_string(),
+ chat_type: "p2p".to_string(),
+ chat_name: Some("Alice".to_string()),
+ message_id: "msg_123".to_string(),
+ sender_open_id: Some("ou_123".to_string()),
+ sender_user_id: None,
+ sender_union_id: None,
+ text: "hello".to_string(),
+ received_at: 123,
+ })
+ .expect("events");
+
+ assert_eq!(
+ events,
+ vec![
+ ProviderEvent::SessionUpserted(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_123".to_string(),
+ display_name: Some("Alice".to_string()),
+ unread_count: 0,
+ last_message_at: Some(123),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }),
+ ProviderEvent::InboundMessage(crate::events::ProviderInboundMessage {
+ session: ProviderSessionRef::new(ProviderKind::Feishu, "chat_123"),
+ message_id: "msg_123".to_string(),
+ text: "hello".to_string(),
+ received_at: 123,
+ }),
+ ]
+ );
+ }
+
+ #[test]
+ fn message_receive_event_skips_non_text_messages() {
+ let envelope = super::FeishuMessageReceiveEnvelope {
+ event: super::FeishuMessageReceiveEvent {
+ sender: super::FeishuEventSender {
+ sender_id: super::FeishuUserId {
+ open_id: Some("ou_123".to_string()),
+ user_id: None,
+ union_id: None,
+ app_id: None,
+ },
+ },
+ message: super::FeishuEventMessage {
+ message_id: "msg_123".to_string(),
+ create_time: serde_json::json!("456"),
+ chat_id: Some("chat_123".to_string()),
+ chat_type: Some("p2p".to_string()),
+ message_type: Some("image".to_string()),
+ msg_type: None,
+ content: Some("{}".to_string()),
+ body: None,
+ },
+ chat: None,
+ },
+ };
+
+ assert_eq!(
+ normalize_message_receive_event(envelope, &FeishuConfig::default(), Path::new("/tmp")),
+ None
+ );
+ }
+
+ #[test]
+ fn message_receive_event_accepts_group_text_messages() {
+ let envelope = super::FeishuMessageReceiveEnvelope {
+ event: super::FeishuMessageReceiveEvent {
+ sender: super::FeishuEventSender {
+ sender_id: super::FeishuUserId {
+ open_id: Some("ou_member".to_string()),
+ user_id: None,
+ union_id: None,
+ app_id: None,
+ },
+ },
+ message: super::FeishuEventMessage {
+ message_id: "msg_group_1".to_string(),
+ create_time: serde_json::json!("456"),
+ chat_id: Some("chat_group_123".to_string()),
+ chat_type: Some("group".to_string()),
+ message_type: Some("text".to_string()),
+ msg_type: None,
+ content: Some("{\"text\":\"hello group\"}".to_string()),
+ body: None,
+ },
+ chat: Some(super::FeishuEventChat {
+ chat_id: "chat_group_123".to_string(),
+ chat_type: Some("group".to_string()),
+ name: Some("tracker".to_string()),
+ }),
+ },
+ };
+
+ assert_eq!(
+ normalize_message_receive_event(envelope, &FeishuConfig::default(), Path::new("/tmp")),
+ Some(vec![
+ ProviderEvent::SessionUpserted(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_group_123".to_string(),
+ display_name: Some("tracker".to_string()),
+ unread_count: 0,
+ last_message_at: Some(456),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }),
+ ProviderEvent::InboundMessage(crate::events::ProviderInboundMessage {
+ session: ProviderSessionRef::new(ProviderKind::Feishu, "chat_group_123"),
+ message_id: "msg_group_1".to_string(),
+ text: "hello group".to_string(),
+ received_at: 456,
+ }),
+ ])
+ );
+ }
+
+ #[test]
+ fn message_receive_event_skips_bot_authored_messages() {
+ let envelope = super::FeishuMessageReceiveEnvelope {
+ event: super::FeishuMessageReceiveEvent {
+ sender: super::FeishuEventSender {
+ sender_id: super::FeishuUserId {
+ open_id: Some("ou_bot".to_string()),
+ user_id: None,
+ union_id: None,
+ app_id: None,
+ },
+ },
+ message: super::FeishuEventMessage {
+ message_id: "msg_bot_1".to_string(),
+ create_time: serde_json::json!("456"),
+ chat_id: Some("chat_group_123".to_string()),
+ chat_type: Some("group".to_string()),
+ message_type: Some("text".to_string()),
+ msg_type: None,
+ content: Some("{\"text\":\"hello group\"}".to_string()),
+ body: None,
+ },
+ chat: Some(super::FeishuEventChat {
+ chat_id: "chat_group_123".to_string(),
+ chat_type: Some("group".to_string()),
+ name: Some("tracker".to_string()),
+ }),
+ },
+ };
+
+ assert_eq!(
+ normalize_message_receive_event(
+ envelope,
+ &FeishuConfig {
+ bot_open_id: Some("ou_bot".to_string()),
+ ..FeishuConfig::default()
+ },
+ Path::new("/tmp"),
+ ),
+ None
+ );
+ }
+
+ #[test]
+ fn message_receive_event_skips_app_authored_messages() {
+ let envelope = super::FeishuMessageReceiveEnvelope {
+ event: super::FeishuMessageReceiveEvent {
+ sender: super::FeishuEventSender {
+ sender_id: super::FeishuUserId {
+ open_id: None,
+ user_id: None,
+ union_id: None,
+ app_id: Some("cli_app_123".to_string()),
+ },
+ },
+ message: super::FeishuEventMessage {
+ message_id: "msg_bot_app_1".to_string(),
+ create_time: serde_json::json!("456"),
+ chat_id: Some("chat_group_123".to_string()),
+ chat_type: Some("group".to_string()),
+ message_type: Some("text".to_string()),
+ msg_type: None,
+ content: Some("{\"text\":\"hello group\"}".to_string()),
+ body: None,
+ },
+ chat: Some(super::FeishuEventChat {
+ chat_id: "chat_group_123".to_string(),
+ chat_type: Some("group".to_string()),
+ name: Some("tracker".to_string()),
+ }),
+ },
+ };
+
+ assert_eq!(
+ normalize_message_receive_event(
+ envelope,
+ &FeishuConfig {
+ app_id: "cli_app_123".to_string(),
+ ..FeishuConfig::default()
+ },
+ Path::new("/tmp"),
+ ),
+ None
+ );
+ }
+
+ #[test]
+ fn message_receive_event_uses_chat_fallbacks_for_group_payloads() {
+ let envelope = super::FeishuMessageReceiveEnvelope {
+ event: super::FeishuMessageReceiveEvent {
+ sender: super::FeishuEventSender {
+ sender_id: super::FeishuUserId {
+ open_id: Some("ou_member".to_string()),
+ user_id: None,
+ union_id: None,
+ app_id: None,
+ },
+ },
+ message: super::FeishuEventMessage {
+ message_id: "msg_group_fallback_1".to_string(),
+ create_time: serde_json::json!(456),
+ chat_id: None,
+ chat_type: None,
+ message_type: None,
+ msg_type: Some("text".to_string()),
+ content: None,
+ body: Some(super::FeishuEventMessageBody {
+ content: Some("{\"text\":\"hello fallback\"}".to_string()),
+ }),
+ },
+ chat: Some(super::FeishuEventChat {
+ chat_id: "chat_group_fallback_123".to_string(),
+ chat_type: Some("group".to_string()),
+ name: Some("tracker".to_string()),
+ }),
+ },
+ };
+
+ assert_eq!(
+ normalize_message_receive_event(envelope, &FeishuConfig::default(), Path::new("/tmp")),
+ Some(vec![
+ ProviderEvent::SessionUpserted(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_group_fallback_123".to_string(),
+ display_name: Some("tracker".to_string()),
+ unread_count: 0,
+ last_message_at: Some(456),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }),
+ ProviderEvent::InboundMessage(crate::events::ProviderInboundMessage {
+ session: ProviderSessionRef::new(
+ ProviderKind::Feishu,
+ "chat_group_fallback_123",
+ ),
+ message_id: "msg_group_fallback_1".to_string(),
+ text: "hello fallback".to_string(),
+ received_at: 456,
+ }),
+ ])
+ );
+ }
+
+ #[test]
+ fn chat_entered_event_creates_discovered_session() {
+ let events = normalize_chat_entered_event(super::FeishuChatEnteredEnvelope {
+ event: super::FeishuChatEnteredEvent {
+ chat_id: "chat_123".to_string(),
+ operator_id: super::FeishuUserId {
+ open_id: Some("ou_123".to_string()),
+ user_id: None,
+ union_id: None,
+ app_id: None,
+ },
+ last_message_create_time: Some("456".to_string()),
+ },
+ });
+
+ assert_eq!(
+ events,
+ vec![ProviderEvent::SessionUpserted(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_123".to_string(),
+ display_name: Some("ou_123".to_string()),
+ unread_count: 0,
+ last_message_at: Some(456),
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ })]
+ );
+ }
+
+ #[test]
+ fn failure_reply_text_uses_first_nonempty_line() {
+ assert_eq!(
+ failure_reply_text("\nboom\nsecond"),
+ "Request failed: boom".to_string()
+ );
+ }
+
+ #[test]
+ fn strip_group_mention_prefix_removes_leading_mentions() {
+ assert_eq!(
+ strip_group_mention_prefix("@_user_1 TRACKER TEST 2"),
+ "TRACKER TEST 2".to_string()
+ );
+ assert_eq!(
+ strip_group_mention_prefix("@bot: hello"),
+ "hello".to_string()
+ );
+ assert_eq!(
+ strip_group_mention_prefix("@bot @helper ping"),
+ "ping".to_string()
+ );
+ }
+}
diff --git a/codex-rs/clawbot/src/provider/feishu/coordination.rs b/codex-rs/clawbot/src/provider/feishu/coordination.rs
new file mode 100644
index 000000000..09e332d4b
--- /dev/null
+++ b/codex-rs/clawbot/src/provider/feishu/coordination.rs
@@ -0,0 +1,1210 @@
+use std::collections::hash_map::DefaultHasher;
+use std::hash::Hash;
+use std::hash::Hasher;
+use std::path::Path;
+use std::sync::Arc;
+use std::time::Duration;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use anyhow::Context;
+use anyhow::Result;
+use anyhow::anyhow;
+use open_lark::openlark_core::api::ApiRequest;
+use open_lark::openlark_core::api::Response;
+use open_lark::openlark_core::http::Transport;
+use serde_json::Map;
+use serde_json::Value;
+use tokio::sync::Mutex;
+
+use crate::config::FeishuConfig;
+use crate::config::FeishuCoordinationConfig;
+
+const HEARTBEAT_TABLE_NAME: &str = "clawbot_coordination_heartbeat";
+const FORCE_TABLE_NAME: &str = "clawbot_coordination_force";
+
+const FIELD_TYPE_TEXT: i64 = 1;
+const FIELD_TYPE_NUMBER: i64 = 2;
+
+const HEARTBEAT_FIELD_KEY: &str = "key";
+const HEARTBEAT_FIELD_APP_ID: &str = "app_id";
+const HEARTBEAT_FIELD_INSTANCE_ID: &str = "instance_id";
+const HEARTBEAT_FIELD_SESSION_ID: &str = "session_id";
+const HEARTBEAT_FIELD_OWNER_PRIORITY: &str = "owner_priority";
+const HEARTBEAT_FIELD_LAST_SEEN_MS: &str = "last_seen_ms";
+const HEARTBEAT_FIELD_TTL_MS: &str = "ttl_ms";
+const HEARTBEAT_FIELD_WS_STATE: &str = "ws_state";
+const HEARTBEAT_FIELD_WORKSPACE_ROOT: &str = "workspace_root";
+
+const FORCE_FIELD_KEY: &str = "key";
+const FORCE_FIELD_APP_ID: &str = "app_id";
+const FORCE_FIELD_TARGET_INSTANCE_ID: &str = "target_instance_id";
+const FORCE_FIELD_TARGET_SESSION_ID: &str = "target_session_id";
+const FORCE_FIELD_FORCE_UNTIL_MS: &str = "force_until_ms";
+const FORCE_FIELD_REQUESTED_AT_MS: &str = "requested_at_ms";
+
+const FEISHU_CODE_WRONG_BASE_TOKEN: i32 = 1_254_003;
+const FEISHU_CODE_WRONG_TABLE_ID: i32 = 1_254_004;
+const FEISHU_CODE_WRONG_FIELD_ID: i32 = 1_254_009;
+const FEISHU_CODE_BASE_TOKEN_NOT_FOUND: i32 = 1_254_040;
+const FEISHU_CODE_TABLE_ID_NOT_FOUND: i32 = 1_254_041;
+const FEISHU_CODE_FIELD_ID_NOT_FOUND: i32 = 1_254_044;
+const FEISHU_CODE_PERMISSION_DENIED: i32 = 1_254_302;
+
+const HEARTBEAT_FIELDS: [RequiredField; 9] = [
+ RequiredField::new(HEARTBEAT_FIELD_KEY, FIELD_TYPE_TEXT),
+ RequiredField::new(HEARTBEAT_FIELD_APP_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(HEARTBEAT_FIELD_INSTANCE_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(HEARTBEAT_FIELD_SESSION_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(HEARTBEAT_FIELD_OWNER_PRIORITY, FIELD_TYPE_NUMBER),
+ RequiredField::new(HEARTBEAT_FIELD_LAST_SEEN_MS, FIELD_TYPE_NUMBER),
+ RequiredField::new(HEARTBEAT_FIELD_TTL_MS, FIELD_TYPE_NUMBER),
+ RequiredField::new(HEARTBEAT_FIELD_WS_STATE, FIELD_TYPE_TEXT),
+ RequiredField::new(HEARTBEAT_FIELD_WORKSPACE_ROOT, FIELD_TYPE_TEXT),
+];
+
+const FORCE_FIELDS: [RequiredField; 6] = [
+ RequiredField::new(FORCE_FIELD_KEY, FIELD_TYPE_TEXT),
+ RequiredField::new(FORCE_FIELD_APP_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(FORCE_FIELD_TARGET_INSTANCE_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(FORCE_FIELD_TARGET_SESSION_ID, FIELD_TYPE_TEXT),
+ RequiredField::new(FORCE_FIELD_FORCE_UNTIL_MS, FIELD_TYPE_NUMBER),
+ RequiredField::new(FORCE_FIELD_REQUESTED_AT_MS, FIELD_TYPE_NUMBER),
+];
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(super) enum WebsocketOwnershipState {
+ Idle,
+ Connected,
+ BackingOff,
+}
+
+impl WebsocketOwnershipState {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Idle => "idle",
+ Self::Connected => "connected",
+ Self::BackingOff => "backing_off",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(super) struct LeadershipSnapshot {
+ pub is_leader: bool,
+ pub leader_instance_id: String,
+ pub leader_session_id: String,
+ pub forced_instance_id: Option,
+}
+
+impl LeadershipSnapshot {
+ pub fn standby_message(&self) -> String {
+ let forced = self
+ .forced_instance_id
+ .as_deref()
+ .map(|instance_id| format!("; force owner {instance_id}"))
+ .unwrap_or_default();
+ format!(
+ "leader is {} (session {}){forced}",
+ self.leader_instance_id, self.leader_session_id
+ )
+ }
+}
+
+#[derive(Debug, Clone)]
+pub(super) struct FeishuBaseCoordinator {
+ client: FeishuBaseClient,
+ app_id: String,
+ workspace_root: String,
+ instance_id: String,
+ session_id: String,
+ owner_priority: i64,
+ heartbeat_interval: Duration,
+ heartbeat_ttl: Duration,
+ force_connect: bool,
+}
+
+impl FeishuBaseCoordinator {
+ pub(super) fn new(workspace_root: &Path, config: &FeishuConfig) -> Result> {
+ let Some(coordination) = config
+ .coordination
+ .clone()
+ .filter(FeishuCoordinationConfig::is_configured)
+ else {
+ return Ok(None);
+ };
+
+ let core_config = super::runtime_loop::build_websocket_config(config)?
+ .build_core_config_with_token_provider();
+ let now_ms = unix_timestamp_ms_now()?;
+ let workspace_root_display = workspace_root.display().to_string();
+ let instance_id = coordination
+ .instance_id
+ .clone()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or_else(|| default_instance_id(&workspace_root_display));
+ let session_id = format!("p{}-{now_ms}", std::process::id());
+ let owner_priority = coordination.owner_priority;
+ let heartbeat_interval = coordination.heartbeat_interval();
+ let heartbeat_ttl = coordination.heartbeat_ttl();
+ let force_connect = coordination.force_connect;
+
+ Ok(Some(Self {
+ client: FeishuBaseClient::new(core_config, coordination),
+ app_id: config.app_id.clone(),
+ workspace_root: workspace_root_display,
+ instance_id,
+ session_id,
+ owner_priority,
+ heartbeat_interval,
+ heartbeat_ttl,
+ force_connect,
+ }))
+ }
+
+ pub(super) fn heartbeat_interval(&self) -> Duration {
+ self.heartbeat_interval
+ }
+
+ pub(super) async fn refresh_leadership(
+ &self,
+ websocket_state: WebsocketOwnershipState,
+ ) -> Result {
+ let now_ms = unix_timestamp_ms_now()?;
+ let heartbeat = HeartbeatLease {
+ key: heartbeat_key(&self.app_id, &self.instance_id),
+ app_id: self.app_id.clone(),
+ instance_id: self.instance_id.clone(),
+ session_id: self.session_id.clone(),
+ owner_priority: self.owner_priority,
+ last_seen_ms: now_ms,
+ ttl_ms: duration_to_millis_i64(self.heartbeat_ttl)?,
+ ws_state: websocket_state.as_str().to_string(),
+ workspace_root: self.workspace_root.clone(),
+ };
+ self.client.upsert_heartbeat(&heartbeat).await?;
+ self.client
+ .sync_force_intent(
+ &self.app_id,
+ &self.instance_id,
+ &self.session_id,
+ now_ms,
+ heartbeat.ttl_ms,
+ self.force_connect,
+ )
+ .await?;
+
+ let leases = self.client.list_heartbeats(&self.app_id).await?;
+ let force_intent = self.client.get_force_intent(&self.app_id).await?;
+ select_leader(&self.instance_id, now_ms, &leases, force_intent.as_ref())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct ResolvedCoordinationTables {
+ heartbeat_table_id: String,
+ force_table_id: String,
+}
+
+#[derive(Debug, Clone)]
+struct FeishuBaseClient {
+ config: open_lark::openlark_core::config::Config,
+ base_token: String,
+ configured_heartbeat_table_id: Option,
+ configured_force_table_id: Option,
+ schema: Arc>>,
+}
+
+impl FeishuBaseClient {
+ fn new(
+ config: open_lark::openlark_core::config::Config,
+ coordination: FeishuCoordinationConfig,
+ ) -> Self {
+ Self {
+ config,
+ base_token: coordination.base_token,
+ configured_heartbeat_table_id: trimmed_non_empty(&coordination.heartbeat_table_id),
+ configured_force_table_id: trimmed_non_empty(&coordination.force_table_id),
+ schema: Arc::new(Mutex::new(None)),
+ }
+ }
+
+ async fn upsert_heartbeat(&self, heartbeat: &HeartbeatLease) -> Result<()> {
+ let tables = self.ensure_schema().await?;
+ let fields = heartbeat.to_fields();
+ match self
+ .find_heartbeat_record_by_key(&tables.heartbeat_table_id, &heartbeat.key)
+ .await?
+ {
+ Some(record) => {
+ self.update_record(&tables.heartbeat_table_id, &record.record_id, fields)
+ .await
+ .context("failed to update Feishu Base heartbeat record")?;
+ }
+ None => {
+ self.create_record(&tables.heartbeat_table_id, fields)
+ .await
+ .context("failed to create Feishu Base heartbeat record")?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn list_heartbeats(&self, app_id: &str) -> Result> {
+ let tables = self.ensure_schema().await?;
+ let mut heartbeats = Vec::new();
+ for record in self.list_records(&tables.heartbeat_table_id).await? {
+ if let Some(heartbeat) = HeartbeatLease::from_record(record)?
+ && heartbeat.app_id == app_id
+ {
+ heartbeats.push(heartbeat);
+ }
+ }
+ Ok(heartbeats)
+ }
+
+ async fn sync_force_intent(
+ &self,
+ app_id: &str,
+ target_instance_id: &str,
+ target_session_id: &str,
+ now_ms: i64,
+ ttl_ms: i64,
+ force_connect: bool,
+ ) -> Result<()> {
+ let tables = self.ensure_schema().await?;
+ let key = force_key(app_id);
+ let existing = self
+ .find_force_record_by_key(&tables.force_table_id, &key)
+ .await?;
+ let force_until_ms = if force_connect {
+ now_ms + ttl_ms
+ } else {
+ now_ms
+ };
+ let should_write = force_connect
+ || existing.as_ref().is_some_and(|record| {
+ record.target_instance_id == target_instance_id && record.force_until_ms > now_ms
+ });
+ if !should_write {
+ return Ok(());
+ }
+
+ let fields = ForceIntentRecord {
+ record_id: existing
+ .as_ref()
+ .and_then(|record| record.record_id.clone()),
+ key,
+ app_id: app_id.to_string(),
+ target_instance_id: target_instance_id.to_string(),
+ target_session_id: target_session_id.to_string(),
+ force_until_ms,
+ requested_at_ms: now_ms,
+ }
+ .to_fields();
+ if let Some(existing) = existing {
+ self.update_record(
+ &tables.force_table_id,
+ &existing
+ .record_id
+ .clone()
+ .context("missing Feishu Base force intent record id")?,
+ fields,
+ )
+ .await
+ .context("failed to update Feishu Base force intent record")?;
+ } else {
+ self.create_record(&tables.force_table_id, fields)
+ .await
+ .context("failed to create Feishu Base force intent record")?;
+ }
+ Ok(())
+ }
+
+ async fn get_force_intent(&self, app_id: &str) -> Result> {
+ let tables = self.ensure_schema().await?;
+ self.find_force_record_by_key(&tables.force_table_id, &force_key(app_id))
+ .await
+ }
+
+ async fn ensure_schema(&self) -> Result {
+ let mut cached = self.schema.lock().await;
+ if let Some(schema) = cached.clone() {
+ return Ok(schema);
+ }
+
+ let mut tables = self.list_tables().await?;
+ let heartbeat_table = self
+ .resolve_or_create_table(
+ &mut tables,
+ self.configured_heartbeat_table_id.as_deref(),
+ HEARTBEAT_TABLE_NAME,
+ )
+ .await?;
+ self.ensure_required_fields(&heartbeat_table, &HEARTBEAT_FIELDS)
+ .await?;
+
+ let force_table = self
+ .resolve_or_create_table(
+ &mut tables,
+ self.configured_force_table_id.as_deref(),
+ FORCE_TABLE_NAME,
+ )
+ .await?;
+ self.ensure_required_fields(&force_table, &FORCE_FIELDS)
+ .await?;
+
+ let schema = ResolvedCoordinationTables {
+ heartbeat_table_id: heartbeat_table.table_id,
+ force_table_id: force_table.table_id,
+ };
+ *cached = Some(schema.clone());
+ Ok(schema)
+ }
+
+ async fn resolve_or_create_table(
+ &self,
+ tables: &mut Vec,
+ configured_table_id: Option<&str>,
+ table_name: &str,
+ ) -> Result {
+ if let Some(table_id) = configured_table_id
+ && let Some(table) = tables.iter().find(|table| table.table_id == table_id)
+ {
+ return Ok(table.clone());
+ }
+
+ if let Some(table) = choose_named_table(tables, table_name) {
+ return Ok(table);
+ }
+
+ self.create_table(table_name).await?;
+ *tables = self.list_tables().await?;
+ choose_named_table(tables, table_name).ok_or_else(|| {
+ anyhow!(
+ "Feishu Base coordination created table {table_name} but could not resolve its table_id"
+ )
+ })
+ }
+
+ async fn ensure_required_fields(
+ &self,
+ table: &BaseTable,
+ required_fields: &[RequiredField],
+ ) -> Result<()> {
+ let fields = self.list_fields(&table.table_id).await?;
+ for required in required_fields {
+ match fields
+ .iter()
+ .find(|field| field.field_name == required.field_name)
+ {
+ Some(existing) if existing.field_type == required.field_type => {}
+ Some(existing) => {
+ return Err(anyhow!(
+ "Feishu Base coordination table {} ({}) has field {} with type {} but expected {}. Repair the table schema or clear the configured table id so clawbot can recreate it.",
+ table.name,
+ table.table_id,
+ required.field_name,
+ field_type_name(existing.field_type),
+ field_type_name(required.field_type),
+ ));
+ }
+ None => {
+ self.create_field(&table.table_id, required)
+ .await
+ .with_context(|| {
+ format!(
+ "failed to create Feishu Base coordination field {} on table {} ({})",
+ required.field_name, table.name, table.table_id
+ )
+ })?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ async fn find_force_record_by_key(
+ &self,
+ table_id: &str,
+ key: &str,
+ ) -> Result> {
+ let mut matches = Vec::new();
+ for record in self.list_records(table_id).await? {
+ if let Some(force_intent) = ForceIntentRecord::from_record(record)?
+ && force_intent.key == key
+ {
+ matches.push(force_intent);
+ }
+ }
+ matches.sort_by(|left, right| {
+ left.requested_at_ms
+ .cmp(&right.requested_at_ms)
+ .then(left.record_id.cmp(&right.record_id))
+ });
+ Ok(matches.pop())
+ }
+
+ async fn find_heartbeat_record_by_key(
+ &self,
+ table_id: &str,
+ key: &str,
+ ) -> Result > {
+ let mut matches = self
+ .list_records(table_id)
+ .await?
+ .into_iter()
+ .filter(|record| {
+ record
+ .fields
+ .as_object()
+ .and_then(|fields| string_field(fields, HEARTBEAT_FIELD_KEY))
+ .is_some_and(|record_key| record_key == key)
+ })
+ .collect::>();
+ matches.sort_by(|left, right| left.record_id.cmp(&right.record_id));
+ Ok(matches.pop())
+ }
+
+ async fn list_tables(&self) -> Result> {
+ let mut page_token = None;
+ let mut tables = Vec::new();
+ loop {
+ let mut request: ApiRequest =
+ ApiRequest::get(tables_url(&self.base_token)).query("page_size", "100");
+ if let Some(token) = page_token.clone() {
+ request = request.query("page_token", token);
+ }
+ let response = self
+ .request_json("list Feishu Base tables", request)
+ .await?;
+ let payload = paged_payload(
+ response
+ .data
+ .as_ref()
+ .context("Feishu Base table list response is missing data")?,
+ "table list",
+ )?;
+ if let Some(items) = payload.get("items").and_then(Value::as_array) {
+ for item in items {
+ if let Some(table) = BaseTable::from_value(item) {
+ tables.push(table);
+ }
+ }
+ }
+ let has_more = payload
+ .get("has_more")
+ .and_then(Value::as_bool)
+ .unwrap_or(false);
+ if !has_more {
+ break;
+ }
+ let Some(next_page_token) = payload
+ .get("page_token")
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .filter(|token| !token.is_empty())
+ .map(str::to_owned)
+ else {
+ break;
+ };
+ page_token = Some(next_page_token);
+ }
+ Ok(tables)
+ }
+
+ async fn create_table(&self, table_name: &str) -> Result<()> {
+ let request: ApiRequest =
+ ApiRequest::post(tables_url(&self.base_token)).json_body(&serde_json::json!({
+ "table": {
+ "name": table_name,
+ "fields": [{
+ "field_name": HEARTBEAT_FIELD_KEY,
+ "type": FIELD_TYPE_TEXT,
+ }],
+ }
+ }));
+ self.request_json(&format!("create Feishu Base table {table_name}"), request)
+ .await?;
+ Ok(())
+ }
+
+ async fn list_fields(&self, table_id: &str) -> Result> {
+ let mut page_token = None;
+ let mut fields = Vec::new();
+ loop {
+ let mut request: ApiRequest =
+ ApiRequest::get(fields_url(&self.base_token, table_id)).query("page_size", "100");
+ if let Some(token) = page_token.clone() {
+ request = request.query("page_token", token);
+ }
+ let response = self
+ .request_json(
+ &format!("list Feishu Base fields for table {table_id}"),
+ request,
+ )
+ .await?;
+ let payload = paged_payload(
+ response
+ .data
+ .as_ref()
+ .context("Feishu Base field list response is missing data")?,
+ "field list",
+ )?;
+ if let Some(items) = payload.get("items").and_then(Value::as_array) {
+ for item in items {
+ if let Some(field) = BaseField::from_value(item) {
+ fields.push(field);
+ }
+ }
+ }
+ let has_more = payload
+ .get("has_more")
+ .and_then(Value::as_bool)
+ .unwrap_or(false);
+ if !has_more {
+ break;
+ }
+ let Some(next_page_token) = payload
+ .get("page_token")
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .filter(|token| !token.is_empty())
+ .map(str::to_owned)
+ else {
+ break;
+ };
+ page_token = Some(next_page_token);
+ }
+ Ok(fields)
+ }
+
+ async fn create_field(&self, table_id: &str, field: &RequiredField) -> Result<()> {
+ let request: ApiRequest = ApiRequest::post(fields_url(&self.base_token, table_id))
+ .json_body(&serde_json::json!({
+ "field_name": field.field_name,
+ "type": field.field_type,
+ }));
+ self.request_json(
+ &format!(
+ "create Feishu Base field {} on table {table_id}",
+ field.field_name
+ ),
+ request,
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn list_records(&self, table_id: &str) -> Result> {
+ let mut page_token = None;
+ let mut records = Vec::new();
+ loop {
+ let mut request: ApiRequest =
+ ApiRequest::get(records_url(&self.base_token, table_id)).query("page_size", "500");
+ if let Some(token) = page_token.clone() {
+ request = request.query("page_token", token);
+ }
+ let response = self
+ .request_json(
+ &format!("list Feishu Base records for table {table_id}"),
+ request,
+ )
+ .await?;
+ let payload = paged_payload(
+ response
+ .data
+ .as_ref()
+ .context("Feishu Base record list response is missing data")?,
+ "record list",
+ )?;
+ if let Some(items) = payload.get("items").and_then(Value::as_array) {
+ for item in items {
+ if let Some(record) = BaseRecord::from_value(item) {
+ records.push(record);
+ }
+ }
+ }
+ let has_more = payload
+ .get("has_more")
+ .and_then(Value::as_bool)
+ .unwrap_or(false);
+ if !has_more {
+ break;
+ }
+ let Some(next_page_token) = payload
+ .get("page_token")
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .filter(|token| !token.is_empty())
+ .map(str::to_owned)
+ else {
+ break;
+ };
+ page_token = Some(next_page_token);
+ }
+ Ok(records)
+ }
+
+ async fn create_record(&self, table_id: &str, fields: Value) -> Result<()> {
+ let request: ApiRequest = ApiRequest::post(records_url(&self.base_token, table_id))
+ .json_body(&serde_json::json!({ "fields": fields }));
+ self.request_json(
+ &format!("create Feishu Base record in table {table_id}"),
+ request,
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn update_record(&self, table_id: &str, record_id: &str, fields: Value) -> Result<()> {
+ let request: ApiRequest =
+ ApiRequest::put(record_url(&self.base_token, table_id, record_id))
+ .json_body(&serde_json::json!({ "fields": fields }));
+ self.request_json(
+ &format!("update Feishu Base record {record_id} in table {table_id}"),
+ request,
+ )
+ .await?;
+ Ok(())
+ }
+
+ async fn request_json(
+ &self,
+ operation: &str,
+ request: ApiRequest,
+ ) -> Result> {
+ let response = Transport::::request(request, &self.config, Some(Default::default()))
+ .await
+ .with_context(|| format!("failed to {operation}"))?;
+ if response.is_success() {
+ Ok(response)
+ } else {
+ Err(classify_feishu_api_error(
+ operation,
+ &self.base_token,
+ &response,
+ ))
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct RequiredField {
+ field_name: &'static str,
+ field_type: i64,
+}
+
+impl RequiredField {
+ const fn new(field_name: &'static str, field_type: i64) -> Self {
+ Self {
+ field_name,
+ field_type,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct BaseTable {
+ table_id: String,
+ name: String,
+}
+
+impl BaseTable {
+ fn from_value(value: &Value) -> Option {
+ Some(Self {
+ table_id: value.get("table_id")?.as_str()?.to_string(),
+ name: value.get("name")?.as_str()?.trim().to_string(),
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct BaseField {
+ field_name: String,
+ field_type: i64,
+}
+
+impl BaseField {
+ fn from_value(value: &Value) -> Option {
+ Some(Self {
+ field_name: value.get("field_name")?.as_str()?.trim().to_string(),
+ field_type: value.get("type")?.as_i64()?,
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+struct BaseRecord {
+ record_id: String,
+ fields: Value,
+}
+
+impl BaseRecord {
+ fn from_value(value: &Value) -> Option {
+ Some(Self {
+ record_id: value.get("record_id")?.as_str()?.to_string(),
+ fields: value.get("fields")?.clone(),
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct HeartbeatLease {
+ key: String,
+ app_id: String,
+ instance_id: String,
+ session_id: String,
+ owner_priority: i64,
+ last_seen_ms: i64,
+ ttl_ms: i64,
+ ws_state: String,
+ workspace_root: String,
+}
+
+impl HeartbeatLease {
+ fn from_record(record: BaseRecord) -> Result> {
+ let Some(fields) = record.fields.as_object() else {
+ return Ok(None);
+ };
+ let Some(app_id) = string_field(fields, HEARTBEAT_FIELD_APP_ID) else {
+ return Ok(None);
+ };
+ let Some(instance_id) = string_field(fields, HEARTBEAT_FIELD_INSTANCE_ID) else {
+ return Ok(None);
+ };
+ let Some(session_id) = string_field(fields, HEARTBEAT_FIELD_SESSION_ID) else {
+ return Ok(None);
+ };
+ Ok(Some(Self {
+ key: string_field(fields, HEARTBEAT_FIELD_KEY)
+ .unwrap_or_else(|| heartbeat_key(&app_id, &instance_id)),
+ app_id,
+ instance_id,
+ session_id,
+ owner_priority: integer_field(fields, HEARTBEAT_FIELD_OWNER_PRIORITY).unwrap_or(0),
+ last_seen_ms: integer_field(fields, HEARTBEAT_FIELD_LAST_SEEN_MS).unwrap_or(0),
+ ttl_ms: integer_field(fields, HEARTBEAT_FIELD_TTL_MS).unwrap_or(0),
+ ws_state: string_field(fields, HEARTBEAT_FIELD_WS_STATE).unwrap_or_default(),
+ workspace_root: string_field(fields, HEARTBEAT_FIELD_WORKSPACE_ROOT)
+ .unwrap_or_default(),
+ }))
+ }
+
+ fn is_active(&self, now_ms: i64) -> bool {
+ self.last_seen_ms + self.ttl_ms >= now_ms
+ }
+
+ fn to_fields(&self) -> Value {
+ serde_json::json!({
+ HEARTBEAT_FIELD_KEY: self.key,
+ HEARTBEAT_FIELD_APP_ID: self.app_id,
+ HEARTBEAT_FIELD_INSTANCE_ID: self.instance_id,
+ HEARTBEAT_FIELD_SESSION_ID: self.session_id,
+ HEARTBEAT_FIELD_OWNER_PRIORITY: self.owner_priority,
+ HEARTBEAT_FIELD_LAST_SEEN_MS: self.last_seen_ms,
+ HEARTBEAT_FIELD_TTL_MS: self.ttl_ms,
+ HEARTBEAT_FIELD_WS_STATE: self.ws_state,
+ HEARTBEAT_FIELD_WORKSPACE_ROOT: self.workspace_root,
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct ForceIntentRecord {
+ record_id: Option,
+ key: String,
+ app_id: String,
+ target_instance_id: String,
+ target_session_id: String,
+ force_until_ms: i64,
+ requested_at_ms: i64,
+}
+
+impl ForceIntentRecord {
+ fn from_record(record: BaseRecord) -> Result> {
+ let Some(fields) = record.fields.as_object() else {
+ return Ok(None);
+ };
+ let Some(app_id) = string_field(fields, FORCE_FIELD_APP_ID) else {
+ return Ok(None);
+ };
+ let Some(target_instance_id) = string_field(fields, FORCE_FIELD_TARGET_INSTANCE_ID) else {
+ return Ok(None);
+ };
+ let Some(target_session_id) = string_field(fields, FORCE_FIELD_TARGET_SESSION_ID) else {
+ return Ok(None);
+ };
+ Ok(Some(Self {
+ record_id: Some(record.record_id),
+ key: string_field(fields, FORCE_FIELD_KEY).unwrap_or_else(|| force_key(&app_id)),
+ app_id,
+ target_instance_id,
+ target_session_id,
+ force_until_ms: integer_field(fields, FORCE_FIELD_FORCE_UNTIL_MS).unwrap_or(0),
+ requested_at_ms: integer_field(fields, FORCE_FIELD_REQUESTED_AT_MS).unwrap_or(0),
+ }))
+ }
+
+ fn is_active(&self, now_ms: i64) -> bool {
+ self.force_until_ms >= now_ms
+ }
+
+ fn to_fields(&self) -> Value {
+ serde_json::json!({
+ FORCE_FIELD_KEY: self.key,
+ FORCE_FIELD_APP_ID: self.app_id,
+ FORCE_FIELD_TARGET_INSTANCE_ID: self.target_instance_id,
+ FORCE_FIELD_TARGET_SESSION_ID: self.target_session_id,
+ FORCE_FIELD_FORCE_UNTIL_MS: self.force_until_ms,
+ FORCE_FIELD_REQUESTED_AT_MS: self.requested_at_ms,
+ })
+ }
+}
+
+fn select_leader(
+ current_instance_id: &str,
+ now_ms: i64,
+ leases: &[HeartbeatLease],
+ force_intent: Option<&ForceIntentRecord>,
+) -> Result {
+ let mut active = leases
+ .iter()
+ .filter(|lease| lease.is_active(now_ms))
+ .cloned()
+ .collect::>();
+ active.sort_by(|left, right| {
+ right
+ .owner_priority
+ .cmp(&left.owner_priority)
+ .then(left.instance_id.cmp(&right.instance_id))
+ .then(left.session_id.cmp(&right.session_id))
+ });
+ let leader =
+ if let Some(force_intent) = force_intent.filter(|intent| intent.is_active(now_ms)) {
+ if let Some(forced_lease) = active
+ .iter()
+ .find(|lease| lease.instance_id == force_intent.target_instance_id)
+ {
+ forced_lease.clone()
+ } else {
+ active.first().cloned().ok_or_else(|| {
+ anyhow!("no active Feishu Base coordination heartbeat rows found")
+ })?
+ }
+ } else {
+ active
+ .first()
+ .cloned()
+ .ok_or_else(|| anyhow!("no active Feishu Base coordination heartbeat rows found"))?
+ };
+ Ok(LeadershipSnapshot {
+ is_leader: leader.instance_id == current_instance_id,
+ leader_instance_id: leader.instance_id,
+ leader_session_id: leader.session_id,
+ forced_instance_id: force_intent
+ .filter(|intent| intent.is_active(now_ms))
+ .map(|intent| intent.target_instance_id.clone()),
+ })
+}
+
+fn choose_named_table(tables: &[BaseTable], table_name: &str) -> Option {
+ tables
+ .iter()
+ .filter(|table| table.name == table_name)
+ .cloned()
+ .min_by(|left, right| left.table_id.cmp(&right.table_id))
+}
+
+fn classify_feishu_api_error(
+ operation: &str,
+ base_token: &str,
+ response: &Response,
+) -> anyhow::Error {
+ let code = response.code();
+ let msg = response.msg().trim();
+ let request_id = response
+ .raw()
+ .request_id
+ .as_deref()
+ .map(|id| format!(", request_id {id}"))
+ .unwrap_or_default();
+
+ match code {
+ FEISHU_CODE_PERMISSION_DENIED => anyhow!(
+ "Feishu Base coordination could not {operation}: permission denied (code {code}{request_id}): {msg}. Grant the app edit/admin access on base {base_token} or add it to a Base role with write permission."
+ ),
+ FEISHU_CODE_WRONG_BASE_TOKEN | FEISHU_CODE_BASE_TOKEN_NOT_FOUND => anyhow!(
+ "Feishu Base coordination could not {operation}: base_token {base_token} is invalid or inaccessible (code {code}{request_id}): {msg}. Check [feishu.coordination].base_token and make sure the app can access that Base."
+ ),
+ FEISHU_CODE_WRONG_TABLE_ID | FEISHU_CODE_TABLE_ID_NOT_FOUND => anyhow!(
+ "Feishu Base coordination could not {operation}: the configured table id is invalid (code {code}{request_id}): {msg}. Clear or repair heartbeat_table_id / force_table_id so clawbot can resolve or recreate its coordination tables."
+ ),
+ FEISHU_CODE_WRONG_FIELD_ID | FEISHU_CODE_FIELD_ID_NOT_FOUND => anyhow!(
+ "Feishu Base coordination could not {operation}: the coordination table schema is missing a required field (code {code}{request_id}): {msg}. Repair the Base schema or let clawbot recreate the coordination tables."
+ ),
+ _ => anyhow!(
+ "Feishu Base coordination could not {operation} (code {code}{request_id}): {msg}"
+ ),
+ }
+}
+
+fn paged_payload<'a>(data: &'a Value, label: &str) -> Result<&'a Value> {
+ data.get("items")
+ .is_some()
+ .then_some(data)
+ .or_else(|| data.get("data"))
+ .with_context(|| format!("Feishu Base {label} response is missing items payload"))
+}
+
+fn default_instance_id(workspace_root: &str) -> String {
+ let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "local".to_string());
+ let mut hasher = DefaultHasher::new();
+ workspace_root.hash(&mut hasher);
+ format!("{hostname}-{}-{:x}", std::process::id(), hasher.finish())
+}
+
+fn heartbeat_key(app_id: &str, instance_id: &str) -> String {
+ format!("{app_id}:{instance_id}")
+}
+
+fn force_key(app_id: &str) -> String {
+ app_id.to_string()
+}
+
+fn tables_url(base_token: &str) -> String {
+ format!("/open-apis/bitable/v1/apps/{base_token}/tables")
+}
+
+fn fields_url(base_token: &str, table_id: &str) -> String {
+ format!("/open-apis/bitable/v1/apps/{base_token}/tables/{table_id}/fields")
+}
+
+fn records_url(base_token: &str, table_id: &str) -> String {
+ format!("/open-apis/bitable/v1/apps/{base_token}/tables/{table_id}/records")
+}
+
+fn record_url(base_token: &str, table_id: &str, record_id: &str) -> String {
+ format!("/open-apis/bitable/v1/apps/{base_token}/tables/{table_id}/records/{record_id}")
+}
+
+fn trimmed_non_empty(value: &str) -> Option {
+ let trimmed = value.trim();
+ (!trimmed.is_empty()).then(|| trimmed.to_string())
+}
+
+fn field_type_name(field_type: i64) -> &'static str {
+ match field_type {
+ FIELD_TYPE_TEXT => "text",
+ FIELD_TYPE_NUMBER => "number",
+ _ => "unknown",
+ }
+}
+
+fn string_field(fields: &Map, key: &str) -> Option {
+ fields
+ .get(key)
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(str::to_string)
+}
+
+fn integer_field(fields: &Map, key: &str) -> Option {
+ let value = fields.get(key)?;
+ value
+ .as_i64()
+ .or_else(|| value.as_u64().and_then(|raw| i64::try_from(raw).ok()))
+ .or_else(|| value.as_f64().map(|raw| raw as i64))
+ .or_else(|| value.as_str().and_then(|raw| raw.parse().ok()))
+}
+
+fn duration_to_millis_i64(duration: Duration) -> Result {
+ i64::try_from(duration.as_millis()).context("duration does not fit into i64 milliseconds")
+}
+
+fn unix_timestamp_ms_now() -> Result {
+ let elapsed = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .context("system clock is before unix epoch")?;
+ i64::try_from(elapsed.as_millis()).context("unix timestamp milliseconds exceed i64")
+}
+
+#[cfg(test)]
+mod tests {
+ use pretty_assertions::assert_eq;
+
+ use super::BaseTable;
+ use super::FORCE_FIELDS;
+ use super::ForceIntentRecord;
+ use super::HEARTBEAT_FIELDS;
+ use super::HeartbeatLease;
+ use super::RequiredField;
+ use super::choose_named_table;
+ use super::classify_feishu_api_error;
+ use super::select_leader;
+
+ #[test]
+ fn select_leader_prefers_higher_priority_then_instance_id() {
+ let leader = select_leader(
+ "instance_b",
+ 10_000,
+ &[
+ heartbeat("instance_b", "session_b", 200, 9_900, 500),
+ heartbeat("instance_a", "session_a", 200, 9_900, 500),
+ heartbeat("instance_c", "session_c", 100, 9_900, 500),
+ ],
+ None,
+ )
+ .expect("leader");
+
+ assert_eq!(
+ leader,
+ super::LeadershipSnapshot {
+ is_leader: false,
+ leader_instance_id: "instance_a".to_string(),
+ leader_session_id: "session_a".to_string(),
+ forced_instance_id: None,
+ }
+ );
+ }
+
+ #[test]
+ fn select_leader_honors_active_force_intent() {
+ let leader = select_leader(
+ "instance_b",
+ 10_000,
+ &[
+ heartbeat("instance_a", "session_a", 200, 9_900, 500),
+ heartbeat("instance_b", "session_b", 100, 9_900, 500),
+ ],
+ Some(&force("instance_b", "session_b", 10_500, 9_999)),
+ )
+ .expect("leader");
+
+ assert_eq!(
+ leader,
+ super::LeadershipSnapshot {
+ is_leader: true,
+ leader_instance_id: "instance_b".to_string(),
+ leader_session_id: "session_b".to_string(),
+ forced_instance_id: Some("instance_b".to_string()),
+ }
+ );
+ }
+
+ #[test]
+ fn select_leader_ignores_expired_force_intent() {
+ let leader = select_leader(
+ "instance_b",
+ 10_000,
+ &[
+ heartbeat("instance_a", "session_a", 200, 9_900, 500),
+ heartbeat("instance_b", "session_b", 100, 9_900, 500),
+ ],
+ Some(&force("instance_b", "session_b", 9_999, 9_900)),
+ )
+ .expect("leader");
+
+ assert_eq!(leader.leader_instance_id, "instance_a");
+ assert_eq!(leader.forced_instance_id, None);
+ }
+
+ #[test]
+ fn select_leader_ignores_force_intent_when_target_is_inactive() {
+ let leader = select_leader(
+ "instance_b",
+ 10_000,
+ &[
+ heartbeat("instance_a", "session_a", 200, 9_900, 500),
+ heartbeat("instance_b", "session_b", 100, 9_000, 500),
+ ],
+ Some(&force("instance_b", "session_b", 10_500, 9_999)),
+ )
+ .expect("leader");
+
+ assert_eq!(leader.leader_instance_id, "instance_a");
+ assert_eq!(leader.forced_instance_id, Some("instance_b".to_string()));
+ }
+
+ #[test]
+ fn choose_named_table_is_deterministic_when_duplicates_exist() {
+ let tables = vec![
+ BaseTable {
+ table_id: "tbl_b".to_string(),
+ name: "clawbot_coordination_heartbeat".to_string(),
+ },
+ BaseTable {
+ table_id: "tbl_a".to_string(),
+ name: "clawbot_coordination_heartbeat".to_string(),
+ },
+ ];
+
+ let chosen =
+ choose_named_table(&tables, "clawbot_coordination_heartbeat").expect("named table");
+ assert_eq!(chosen.table_id, "tbl_a");
+ }
+
+ #[test]
+ fn required_field_sets_match_expected_shape() {
+ assert_eq!(
+ HEARTBEAT_FIELDS.first(),
+ Some(&RequiredField::new("key", 1))
+ );
+ assert_eq!(FORCE_FIELDS.first(), Some(&RequiredField::new("key", 1)));
+ }
+
+ #[test]
+ fn permission_error_is_actionable() {
+ let response = open_lark::openlark_core::api::Response::error(
+ 1_254_302,
+ "The role has no permissions.",
+ );
+ let message = classify_feishu_api_error(
+ "create Feishu Base table clawbot_coordination_heartbeat",
+ "bascn_test",
+ &response,
+ )
+ .to_string();
+
+ assert!(message.contains("Grant the app edit/admin access"));
+ assert!(message.contains("bascn_test"));
+ }
+
+ fn heartbeat(
+ instance_id: &str,
+ session_id: &str,
+ owner_priority: i64,
+ last_seen_ms: i64,
+ ttl_ms: i64,
+ ) -> HeartbeatLease {
+ HeartbeatLease {
+ key: format!("app_test:{instance_id}"),
+ app_id: "app_test".to_string(),
+ instance_id: instance_id.to_string(),
+ session_id: session_id.to_string(),
+ owner_priority,
+ last_seen_ms,
+ ttl_ms,
+ ws_state: "idle".to_string(),
+ workspace_root: "/tmp/workspace".to_string(),
+ }
+ }
+
+ fn force(
+ target_instance_id: &str,
+ target_session_id: &str,
+ force_until_ms: i64,
+ requested_at_ms: i64,
+ ) -> ForceIntentRecord {
+ ForceIntentRecord {
+ record_id: Some("rec_force".to_string()),
+ key: "app_test".to_string(),
+ app_id: "app_test".to_string(),
+ target_instance_id: target_instance_id.to_string(),
+ target_session_id: target_session_id.to_string(),
+ force_until_ms,
+ requested_at_ms,
+ }
+ }
+}
diff --git a/codex-rs/clawbot/src/provider/feishu/runtime_loop.rs b/codex-rs/clawbot/src/provider/feishu/runtime_loop.rs
new file mode 100644
index 000000000..4ae597c9b
--- /dev/null
+++ b/codex-rs/clawbot/src/provider/feishu/runtime_loop.rs
@@ -0,0 +1,327 @@
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::time::Duration;
+use std::time::Instant;
+
+use anyhow::Result;
+use anyhow::anyhow;
+use open_lark::openlark_client;
+use open_lark::openlark_client::ws_client::EventDispatcherHandler;
+use open_lark::openlark_client::ws_client::LarkWsClient;
+use tokio::sync::mpsc;
+use tokio::task::JoinHandle;
+
+use super::coordination::FeishuBaseCoordinator;
+use super::coordination::WebsocketOwnershipState;
+use super::provider_events_from_payload;
+use super::runtime_state;
+use crate::append_diagnostic_event;
+use crate::config::FeishuConfig;
+use crate::model::ConnectionStatus;
+use crate::provider::ProviderEvent;
+
+const INITIAL_RECONNECT_DELAY: Duration = Duration::from_secs(2);
+const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30);
+
+pub(super) async fn run_with_reconnect(
+ workspace_root: PathBuf,
+ config: FeishuConfig,
+ provider_event_tx: mpsc::UnboundedSender,
+) -> Result<()> {
+ if !config.has_api_credentials() {
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Unconfigured,
+ Some("missing app_id/app_secret".to_string()),
+ )?));
+ return Err(anyhow!("missing app_id/app_secret"));
+ }
+
+ if let Some(coordination) = FeishuBaseCoordinator::new(workspace_root.as_path(), &config)? {
+ return run_with_coordination(workspace_root, config, provider_event_tx, coordination)
+ .await;
+ }
+
+ let mut reconnect_delay = INITIAL_RECONNECT_DELAY;
+ loop {
+ match run_once(workspace_root.as_path(), &config, &provider_event_tx).await {
+ Ok(()) => {
+ emit_reconnect_state(
+ workspace_root.as_path(),
+ &provider_event_tx,
+ reconnect_delay,
+ Ok(()),
+ )?;
+ }
+ Err(error) => {
+ emit_reconnect_state(
+ workspace_root.as_path(),
+ &provider_event_tx,
+ reconnect_delay,
+ Err(&error),
+ )?;
+ }
+ }
+
+ tokio::time::sleep(reconnect_delay).await;
+ reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY);
+ }
+}
+
+async fn run_with_coordination(
+ workspace_root: PathBuf,
+ config: FeishuConfig,
+ provider_event_tx: mpsc::UnboundedSender,
+ coordination: FeishuBaseCoordinator,
+) -> Result<()> {
+ let mut reconnect_delay = INITIAL_RECONNECT_DELAY;
+ let mut reconnect_at = None::;
+ let mut websocket_task = None::>>;
+ let mut last_standby_message = None::;
+
+ loop {
+ if let Some(handle) = websocket_task.as_ref()
+ && handle.is_finished()
+ {
+ let result = websocket_task
+ .take()
+ .expect("finished websocket task should still be present")
+ .await
+ .map_err(|error| anyhow!("Feishu websocket runtime task failed: {error}"))?;
+ emit_reconnect_state(
+ workspace_root.as_path(),
+ &provider_event_tx,
+ reconnect_delay,
+ result.as_ref().map(|_| ()).map_err(|error| error),
+ )?;
+ reconnect_at = Some(Instant::now() + reconnect_delay);
+ reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY);
+ }
+
+ let websocket_state = websocket_ownership_state(&websocket_task, reconnect_at);
+ let leadership = match coordination.refresh_leadership(websocket_state).await {
+ Ok(leadership) => leadership,
+ Err(error) => {
+ if let Some(handle) = websocket_task.take() {
+ handle.abort();
+ let _ = handle.await;
+ }
+ let _ = append_diagnostic_event(
+ workspace_root.as_path(),
+ "feishu.coordination_failed",
+ serde_json::json!({
+ "error": error.to_string(),
+ "reconnect_delay_secs": reconnect_delay.as_secs(),
+ }),
+ );
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Error,
+ Some(format!(
+ "Feishu Base coordination failed: {error}; retrying in {}s",
+ reconnect_delay.as_secs()
+ )),
+ )?));
+ last_standby_message = None;
+ reconnect_at = Some(Instant::now() + reconnect_delay);
+ reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY);
+ tokio::time::sleep(reconnect_delay).await;
+ continue;
+ }
+ };
+
+ if leadership.is_leader {
+ last_standby_message = None;
+ if websocket_task.is_none() && backoff_elapsed(reconnect_at) {
+ websocket_task = Some(spawn_websocket_task(
+ workspace_root.clone(),
+ config.clone(),
+ provider_event_tx.clone(),
+ ));
+ reconnect_at = None;
+ reconnect_delay = INITIAL_RECONNECT_DELAY;
+ }
+ } else {
+ reconnect_delay = INITIAL_RECONNECT_DELAY;
+ reconnect_at = None;
+ if let Some(handle) = websocket_task.take() {
+ handle.abort();
+ let _ = handle.await;
+ let _ = append_diagnostic_event(
+ workspace_root.as_path(),
+ "feishu.runtime_leadership_released",
+ serde_json::json!({
+ "leader_instance_id": leadership.leader_instance_id,
+ "leader_session_id": leadership.leader_session_id,
+ }),
+ );
+ }
+
+ let standby_message = format!("Feishu standby: {}", leadership.standby_message());
+ if last_standby_message.as_deref() != Some(standby_message.as_str()) {
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Disconnected,
+ Some(standby_message.clone()),
+ )?));
+ last_standby_message = Some(standby_message);
+ }
+ }
+
+ tokio::time::sleep(sleep_duration(
+ coordination.heartbeat_interval(),
+ reconnect_at,
+ ))
+ .await;
+ }
+}
+
+async fn run_once(
+ workspace_root: &Path,
+ config: &FeishuConfig,
+ provider_event_tx: &mpsc::UnboundedSender,
+) -> Result<()> {
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Connecting,
+ /*last_error*/ None,
+ )?));
+
+ let ws_config = Arc::new(build_websocket_config(config)?);
+ let (payload_tx, mut payload_rx) = mpsc::unbounded_channel::>();
+ let event_handler = EventDispatcherHandler::builder()
+ .payload_sender(payload_tx)
+ .build();
+ let payload_provider_event_tx = provider_event_tx.clone();
+ let payload_config = config.clone();
+ let payload_workspace_root = workspace_root.to_path_buf();
+ let payload_task = tokio::spawn(async move {
+ while let Some(payload) = payload_rx.recv().await {
+ let _ = append_diagnostic_event(
+ payload_workspace_root.as_path(),
+ "feishu.raw_payload",
+ payload_debug_value(&payload),
+ );
+ for event in provider_events_from_payload(
+ &payload,
+ &payload_config,
+ payload_workspace_root.as_path(),
+ ) {
+ let _ = payload_provider_event_tx.send(event);
+ }
+ }
+ });
+
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.runtime_connected",
+ serde_json::json!({}),
+ );
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Connected,
+ /*last_error*/ None,
+ )?));
+
+ let websocket_result =
+ tokio::spawn(async move { LarkWsClient::open(ws_config, event_handler).await })
+ .await
+ .map_err(|error| anyhow!("Feishu websocket runtime task failed: {error}"))?
+ .map_err(|error| anyhow!("Feishu websocket runtime failed: {error}"));
+ payload_task.abort();
+ let _ = payload_task.await;
+ websocket_result
+}
+
+pub(super) fn build_websocket_config(config: &FeishuConfig) -> Result {
+ openlark_client::Config::builder()
+ .app_id(config.app_id.clone())
+ .app_secret(config.app_secret.clone())
+ .timeout(Duration::from_secs(30))
+ .build()
+ .map_err(|error| anyhow!("failed to build Feishu websocket config: {error}"))
+}
+
+fn payload_debug_value(payload: &[u8]) -> serde_json::Value {
+ serde_json::from_slice(payload).unwrap_or_else(|_| {
+ serde_json::json!({
+ "raw": String::from_utf8_lossy(payload),
+ })
+ })
+}
+
+fn spawn_websocket_task(
+ workspace_root: PathBuf,
+ config: FeishuConfig,
+ provider_event_tx: mpsc::UnboundedSender,
+) -> JoinHandle> {
+ tokio::spawn(
+ async move { run_once(workspace_root.as_path(), &config, &provider_event_tx).await },
+ )
+}
+
+fn emit_reconnect_state(
+ workspace_root: &Path,
+ provider_event_tx: &mpsc::UnboundedSender,
+ reconnect_delay: Duration,
+ result: Result<(), &anyhow::Error>,
+) -> Result<()> {
+ match result {
+ Ok(()) => {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.runtime_disconnected",
+ serde_json::json!({
+ "reconnect_delay_secs": reconnect_delay.as_secs(),
+ }),
+ );
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Disconnected,
+ Some(format!(
+ "Feishu websocket runtime exited; reconnecting in {}s",
+ reconnect_delay.as_secs()
+ )),
+ )?));
+ }
+ Err(error) => {
+ let _ = append_diagnostic_event(
+ workspace_root,
+ "feishu.runtime_failed",
+ serde_json::json!({
+ "error": error.to_string(),
+ "reconnect_delay_secs": reconnect_delay.as_secs(),
+ }),
+ );
+ let _ = provider_event_tx.send(ProviderEvent::RuntimeStateUpdated(runtime_state(
+ ConnectionStatus::Error,
+ Some(format!(
+ "Feishu websocket runtime failed: {error}; reconnecting in {}s",
+ reconnect_delay.as_secs()
+ )),
+ )?));
+ }
+ }
+ Ok(())
+}
+
+fn websocket_ownership_state(
+ websocket_task: &Option>>,
+ reconnect_at: Option,
+) -> WebsocketOwnershipState {
+ if websocket_task.is_some() {
+ return WebsocketOwnershipState::Connected;
+ }
+ if !backoff_elapsed(reconnect_at) {
+ return WebsocketOwnershipState::BackingOff;
+ }
+ WebsocketOwnershipState::Idle
+}
+
+fn backoff_elapsed(reconnect_at: Option) -> bool {
+ reconnect_at.is_none_or(|instant| instant <= Instant::now())
+}
+
+fn sleep_duration(heartbeat_interval: Duration, reconnect_at: Option) -> Duration {
+ if let Some(reconnect_at) = reconnect_at {
+ return reconnect_at
+ .saturating_duration_since(Instant::now())
+ .min(heartbeat_interval);
+ }
+ heartbeat_interval
+}
diff --git a/codex-rs/clawbot/src/provider/feishu/sync.rs b/codex-rs/clawbot/src/provider/feishu/sync.rs
new file mode 100644
index 000000000..a8b6a0aff
--- /dev/null
+++ b/codex-rs/clawbot/src/provider/feishu/sync.rs
@@ -0,0 +1,255 @@
+use anyhow::Context;
+use anyhow::Result;
+use open_lark::openlark_communication::im::im::v1::chat::get::GetChatRequest;
+use open_lark::openlark_communication::im::im::v1::chat::list::ListChatsRequest;
+use open_lark::openlark_communication::im::im::v1::chat::models::ChatSortType;
+use open_lark::openlark_communication::im::im::v1::message::models::UserIdType;
+use serde::Deserialize;
+
+use crate::model::ProviderKind;
+use crate::model::ProviderSession;
+use crate::model::SessionStatus;
+
+pub(super) async fn discover_supported_sessions(
+ config: &open_lark::openlark_core::config::Config,
+) -> Result> {
+ let mut sessions = Vec::new();
+ let mut page_token = None;
+
+ loop {
+ let response = list_chat_page(config, page_token.clone()).await?;
+ let next_page_token = response.page_token.clone();
+
+ for chat in response.items {
+ if let Some(session) = load_supported_session(config, chat).await? {
+ sessions.push(session);
+ }
+ }
+
+ if !response.has_more {
+ break;
+ }
+
+ let Some(token) = next_page_token.filter(|token| !token.is_empty()) else {
+ break;
+ };
+ page_token = Some(token);
+ }
+
+ sessions.sort_by(|left, right| left.session_id.cmp(&right.session_id));
+ sessions.dedup_by(|left, right| left.session_id == right.session_id);
+ Ok(sessions)
+}
+
+async fn list_chat_page(
+ config: &open_lark::openlark_core::config::Config,
+ page_token: Option,
+) -> Result {
+ let mut request = ListChatsRequest::new(config.clone())
+ .user_id_type(UserIdType::OpenId)
+ .sort_type(ChatSortType::ByActiveTimeDesc)
+ .page_size(100);
+ if let Some(token) = page_token {
+ request = request.page_token(token);
+ }
+
+ let response = request
+ .execute()
+ .await
+ .context("failed to list Feishu chats")?;
+ serde_json::from_value(response).context("failed to parse Feishu chat list response")
+}
+
+async fn load_supported_session(
+ config: &open_lark::openlark_core::config::Config,
+ chat: FeishuChatListItem,
+) -> Result> {
+ let chat_id = chat.chat_id.clone();
+ let response = GetChatRequest::new(config.clone())
+ .chat_id(chat_id.clone())
+ .user_id_type(UserIdType::OpenId)
+ .execute()
+ .await
+ .with_context(|| format!("failed to load Feishu chat {chat_id}"))?;
+ let details: FeishuChatDetails = serde_json::from_value(response)
+ .with_context(|| format!("failed to parse Feishu chat details for {chat_id}"))?;
+
+ if !is_supported_chat(&details) {
+ return Ok(None);
+ }
+
+ Ok(Some(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: chat.chat_id,
+ display_name: first_non_empty([chat.name, details.name]),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }))
+}
+
+fn first_non_empty(values: [Option; 2]) -> Option {
+ values
+ .into_iter()
+ .flatten()
+ .map(|value| value.trim().to_string())
+ .find(|value| !value.is_empty())
+}
+
+fn is_supported_chat(details: &FeishuChatDetails) -> bool {
+ is_private_chat(details) || is_group_chat(details)
+}
+
+fn is_private_chat(details: &FeishuChatDetails) -> bool {
+ details.chat_type.as_deref() == Some("private")
+ || details.chat_mode.as_deref() == Some("p2p")
+ || details.r#type.as_deref() == Some("p2p")
+}
+
+fn is_group_chat(details: &FeishuChatDetails) -> bool {
+ details.chat_type.as_deref() == Some("group")
+ || details.chat_mode.as_deref() == Some("group")
+ || details.r#type.as_deref() == Some("group")
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+struct FeishuChatListResponse {
+ #[serde(default)]
+ items: Vec,
+ #[serde(default)]
+ page_token: Option,
+ #[serde(default)]
+ has_more: bool,
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+struct FeishuChatListItem {
+ chat_id: String,
+ name: Option,
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+struct FeishuChatDetails {
+ name: Option,
+ chat_mode: Option,
+ chat_type: Option,
+ #[serde(rename = "type")]
+ r#type: Option,
+}
+
+#[cfg(test)]
+mod tests {
+ use pretty_assertions::assert_eq;
+
+ use super::FeishuChatDetails;
+ use super::FeishuChatListItem;
+ use super::FeishuChatListResponse;
+ use super::first_non_empty;
+ use super::is_group_chat;
+ use super::is_private_chat;
+ use super::is_supported_chat;
+
+ #[test]
+ fn parse_list_chat_response_with_items() {
+ let response: FeishuChatListResponse = serde_json::from_value(serde_json::json!({
+ "items": [
+ { "chat_id": "oc_1", "name": "Alice" },
+ { "chat_id": "oc_2", "name": "Bob" }
+ ],
+ "page_token": "next_token",
+ "has_more": true
+ }))
+ .expect("response");
+
+ assert_eq!(
+ response,
+ FeishuChatListResponse {
+ items: vec![
+ FeishuChatListItem {
+ chat_id: "oc_1".to_string(),
+ name: Some("Alice".to_string()),
+ },
+ FeishuChatListItem {
+ chat_id: "oc_2".to_string(),
+ name: Some("Bob".to_string()),
+ },
+ ],
+ page_token: Some("next_token".to_string()),
+ has_more: true,
+ }
+ );
+ }
+
+ #[test]
+ fn parse_chat_details_with_type_field() {
+ let response: FeishuChatDetails = serde_json::from_value(serde_json::json!({
+ "chat_id": "oc_1",
+ "name": "Alice",
+ "type": "p2p"
+ }))
+ .expect("response");
+
+ assert_eq!(
+ response,
+ FeishuChatDetails {
+ name: Some("Alice".to_string()),
+ chat_mode: None,
+ chat_type: None,
+ r#type: Some("p2p".to_string()),
+ }
+ );
+ }
+
+ #[test]
+ fn private_chat_detection_accepts_private_variants() {
+ assert_eq!(
+ is_private_chat(&FeishuChatDetails {
+ name: Some("Alice".to_string()),
+ chat_mode: Some("group".to_string()),
+ chat_type: Some("private".to_string()),
+ r#type: None,
+ }),
+ true
+ );
+ assert_eq!(
+ is_private_chat(&FeishuChatDetails {
+ name: Some("Alice".to_string()),
+ chat_mode: Some("p2p".to_string()),
+ chat_type: Some("group".to_string()),
+ r#type: None,
+ }),
+ true
+ );
+ }
+
+ #[test]
+ fn group_chat_detection_accepts_group_variants() {
+ assert_eq!(
+ is_group_chat(&FeishuChatDetails {
+ name: Some("tracker".to_string()),
+ chat_mode: Some("group".to_string()),
+ chat_type: Some("group".to_string()),
+ r#type: None,
+ }),
+ true
+ );
+ assert_eq!(
+ is_supported_chat(&FeishuChatDetails {
+ name: Some("tracker".to_string()),
+ chat_mode: None,
+ chat_type: Some("group".to_string()),
+ r#type: None,
+ }),
+ true
+ );
+ }
+
+ #[test]
+ fn first_non_empty_skips_blank_values() {
+ assert_eq!(
+ first_non_empty([Some(" ".to_string()), Some("Alice".to_string())]),
+ Some("Alice".to_string())
+ );
+ }
+}
diff --git a/codex-rs/clawbot/src/provider/mod.rs b/codex-rs/clawbot/src/provider/mod.rs
new file mode 100644
index 000000000..a7304ff23
--- /dev/null
+++ b/codex-rs/clawbot/src/provider/mod.rs
@@ -0,0 +1,30 @@
+mod feishu;
+
+use crate::events::ProviderInboundMessage;
+use crate::model::ProviderMessageRef;
+use crate::model::ProviderRuntimeState;
+use crate::model::ProviderSession;
+use crate::model::ProviderSessionRef;
+
+pub use feishu::FeishuProviderRuntime;
+pub use feishu::failure_reply_text as feishu_failure_reply_text;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProviderOutboundTextMessage {
+ pub session: ProviderSessionRef,
+ pub text: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProviderOutboundReaction {
+ pub target: ProviderMessageRef,
+ pub emoji_type: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProviderEvent {
+ RuntimeStateUpdated(ProviderRuntimeState),
+ SessionUpserted(ProviderSession),
+ SessionRemoved(ProviderSessionRef),
+ InboundMessage(ProviderInboundMessage),
+}
diff --git a/codex-rs/clawbot/src/runtime.rs b/codex-rs/clawbot/src/runtime.rs
new file mode 100644
index 000000000..3b783f320
--- /dev/null
+++ b/codex-rs/clawbot/src/runtime.rs
@@ -0,0 +1,892 @@
+use std::collections::HashSet;
+use std::path::PathBuf;
+use std::time::SystemTime;
+use std::time::UNIX_EPOCH;
+
+use anyhow::Context;
+use anyhow::Result;
+
+use crate::config::ClawbotTurnMode;
+use crate::config::FeishuConfig;
+use crate::model::CachedUnreadMessage;
+use crate::model::ClawbotSnapshot;
+use crate::model::ForwardingDirection;
+use crate::model::ForwardingState;
+use crate::model::InboundMessageReceipt;
+use crate::model::ProviderKind;
+use crate::model::ProviderSession;
+use crate::model::ProviderSessionRef;
+use crate::model::SessionBinding;
+use crate::model::SessionStatus;
+use crate::provider::FeishuProviderRuntime;
+use crate::provider::ProviderEvent;
+use crate::store::ClawbotStore;
+
+#[derive(Debug)]
+pub struct ClawbotRuntime {
+ store: ClawbotStore,
+ snapshot: ClawbotSnapshot,
+}
+
+impl ClawbotRuntime {
+ pub fn load(workspace_root: PathBuf) -> Result {
+ let store = ClawbotStore::new(workspace_root);
+ let snapshot = store.load_snapshot()?;
+ Ok(Self { store, snapshot })
+ }
+
+ pub fn reload(&mut self) -> Result<&ClawbotSnapshot> {
+ self.snapshot = self.store.load_snapshot()?;
+ Ok(&self.snapshot)
+ }
+
+ pub fn snapshot(&self) -> &ClawbotSnapshot {
+ &self.snapshot
+ }
+
+ pub fn store(&self) -> &ClawbotStore {
+ &self.store
+ }
+
+ pub fn feishu_provider(&self) -> Option {
+ self.snapshot.config.feishu.clone().map(|config| {
+ FeishuProviderRuntime::new(self.store.workspace_root().to_path_buf(), config)
+ })
+ }
+
+ pub fn update_feishu_config(
+ &mut self,
+ feishu: Option,
+ ) -> Result<&ClawbotSnapshot> {
+ self.snapshot.config.feishu = feishu.filter(|config| !config.is_empty());
+ self.store.save_config(&self.snapshot.config)?;
+ self.reload()
+ }
+
+ pub fn update_turn_mode(&mut self, mode: ClawbotTurnMode) -> Result<&ClawbotSnapshot> {
+ self.snapshot.config.turn_mode = mode;
+ self.store.save_config(&self.snapshot.config)?;
+ self.reload()
+ }
+
+ pub async fn scan_feishu_sessions(&mut self) -> Result<&ClawbotSnapshot> {
+ let provider = self
+ .feishu_provider()
+ .context("Feishu credentials are not configured")?;
+ let discovered_sessions = provider
+ .scan_sessions()
+ .await?
+ .into_iter()
+ .filter_map(|event| match event {
+ ProviderEvent::SessionUpserted(session) => Some(session),
+ ProviderEvent::RuntimeStateUpdated(_)
+ | ProviderEvent::SessionRemoved(_)
+ | ProviderEvent::InboundMessage(_) => None,
+ })
+ .collect::>();
+ self.reconcile_feishu_sessions(discovered_sessions)
+ }
+
+ pub fn can_bind_feishu_session(&self, session: &ProviderSessionRef) -> Result {
+ if session.provider != ProviderKind::Feishu {
+ return Ok(false);
+ }
+
+ if self
+ .snapshot
+ .sessions
+ .iter()
+ .any(|existing| existing.session_ref() == *session)
+ || self.load_binding_for_session(session)?.is_some()
+ || self
+ .store
+ .load_unread_messages()?
+ .into_iter()
+ .any(|message| message.session_ref() == *session)
+ || self
+ .store
+ .load_inbound_receipts()?
+ .into_iter()
+ .any(|receipt| receipt.session_ref() == *session)
+ {
+ return Ok(true);
+ }
+
+ Ok(self.snapshot.config.feishu.as_ref().is_some_and(|config| {
+ config.bot_open_id.as_deref() == Some(session.session_id.as_str())
+ || config.bot_user_id.as_deref() == Some(session.session_id.as_str())
+ }))
+ }
+
+ pub fn clear_unbound_feishu_sessions(&mut self) -> Result<&ClawbotSnapshot> {
+ let bound_sessions = self
+ .store
+ .load_bindings()?
+ .into_iter()
+ .map(|binding| binding.session_ref())
+ .collect::>();
+ let sessions = self
+ .store
+ .load_sessions()?
+ .into_iter()
+ .filter(|session| {
+ session.provider != ProviderKind::Feishu
+ || bound_sessions.contains(&session.session_ref())
+ })
+ .collect::>();
+ let unread_messages = self
+ .store
+ .load_unread_messages()?
+ .into_iter()
+ .filter(|message| {
+ message.provider != ProviderKind::Feishu
+ || bound_sessions.contains(&message.session_ref())
+ })
+ .collect::>();
+
+ self.store.save_sessions(&sessions)?;
+ self.store.save_unread_messages(&unread_messages)?;
+ self.reload()
+ }
+
+ pub fn persist_session(&mut self, session: ProviderSession) -> Result<&ClawbotSnapshot> {
+ self.store.upsert_session(session)?;
+ self.reload()
+ }
+
+ pub fn persist_binding(&mut self, binding: SessionBinding) -> Result<&ClawbotSnapshot> {
+ self.store.upsert_binding(binding)?;
+ self.reload()
+ }
+
+ pub fn connect_session_to_thread(
+ &mut self,
+ session: &ProviderSessionRef,
+ thread_id: String,
+ owner_primary_thread_id: Option,
+ ) -> Result<&ClawbotSnapshot> {
+ let now = unix_timestamp_now()?;
+ let mut bindings = self.store.load_bindings()?;
+ let mut sessions = self.store.load_sessions()?;
+ let existing_binding = bindings
+ .iter()
+ .find(|binding| binding.session_ref() == *session)
+ .cloned();
+
+ for binding in &bindings {
+ if binding.thread_id == thread_id
+ && binding.session_ref() != *session
+ && let Some(existing_session) = sessions
+ .iter_mut()
+ .find(|existing| existing.session_ref() == binding.session_ref())
+ {
+ existing_session.bound_thread_id = None;
+ existing_session.status = SessionStatus::Discovered;
+ }
+ }
+
+ bindings
+ .retain(|binding| binding.thread_id != thread_id || binding.session_ref() == *session);
+ if let Some(binding) = bindings
+ .iter_mut()
+ .find(|binding| binding.session_ref() == *session)
+ {
+ binding.thread_id = thread_id.clone();
+ binding.owner_primary_thread_id = owner_primary_thread_id.clone();
+ binding.updated_at = now;
+ } else {
+ bindings.push(SessionBinding {
+ provider: session.provider,
+ session_id: session.session_id.clone(),
+ thread_id: thread_id.clone(),
+ owner_primary_thread_id,
+ inbound_forwarding_enabled: true,
+ outbound_forwarding_enabled: true,
+ created_at: existing_binding
+ .as_ref()
+ .map_or(now, |binding| binding.created_at),
+ updated_at: now,
+ });
+ }
+
+ if let Some(provider_session) = sessions
+ .iter_mut()
+ .find(|provider_session| provider_session.session_ref() == *session)
+ {
+ provider_session.bound_thread_id = Some(thread_id.clone());
+ provider_session.status = SessionStatus::Bound;
+ } else {
+ sessions.push(ProviderSession {
+ provider: session.provider,
+ session_id: session.session_id.clone(),
+ display_name: None,
+ unread_count: self.unread_count_for_session(session)?,
+ last_message_at: None,
+ status: SessionStatus::Bound,
+ bound_thread_id: Some(thread_id),
+ });
+ }
+
+ self.store.save_bindings(&bindings)?;
+ self.store.save_sessions(&sessions)?;
+ self.reload()
+ }
+
+ pub fn load_binding_for_thread(&self, thread_id: &str) -> Result> {
+ Ok(self
+ .store
+ .load_bindings()?
+ .into_iter()
+ .find(|binding| binding.thread_id == thread_id))
+ }
+
+ pub fn load_binding_for_session(
+ &self,
+ session: &ProviderSessionRef,
+ ) -> Result > {
+ Ok(self
+ .store
+ .load_bindings()?
+ .into_iter()
+ .find(|binding| binding.session_ref() == *session))
+ }
+
+ pub fn bound_session_for_thread(&self, thread_id: &str) -> Result > {
+ Ok(self
+ .load_binding_for_thread(thread_id)?
+ .as_ref()
+ .map(SessionBinding::session_ref))
+ }
+
+ pub fn disconnect_thread(&mut self, thread_id: &str) -> Result > {
+ let Some(binding) = self.load_binding_for_thread(thread_id)? else {
+ return Ok(None);
+ };
+ let session = binding.session_ref();
+ let mut bindings = self.store.load_bindings()?;
+ bindings.retain(|candidate| candidate.thread_id != thread_id);
+ self.store.save_bindings(&bindings)?;
+
+ let mut sessions = self.store.load_sessions()?;
+ if let Some(existing) = sessions
+ .iter_mut()
+ .find(|candidate| candidate.session_ref() == session)
+ {
+ existing.bound_thread_id = None;
+ existing.status = SessionStatus::Discovered;
+ }
+ self.store.save_sessions(&sessions)?;
+ self.reload()?;
+ Ok(Some(session))
+ }
+
+ pub fn set_forwarding_state_for_thread(
+ &mut self,
+ thread_id: &str,
+ direction: ForwardingDirection,
+ state: ForwardingState,
+ ) -> Result > {
+ let mut bindings = self.store.load_bindings()?;
+ let Some(binding) = bindings
+ .iter_mut()
+ .find(|candidate| candidate.thread_id == thread_id)
+ else {
+ return Ok(None);
+ };
+ let next_enabled = matches!(state, ForwardingState::Enabled);
+ match direction {
+ ForwardingDirection::Inbound => binding.inbound_forwarding_enabled = next_enabled,
+ ForwardingDirection::Outbound => binding.outbound_forwarding_enabled = next_enabled,
+ }
+ binding.updated_at = unix_timestamp_now()?;
+ let updated = binding.clone();
+ self.store.save_bindings(&bindings)?;
+ self.reload()?;
+ Ok(Some(updated))
+ }
+
+ pub fn take_next_unread_message(
+ &mut self,
+ session: &ProviderSessionRef,
+ ) -> Result > {
+ let message = self.store.take_next_unread_message(session)?;
+ if message.is_some()
+ && let Some(mut provider_session) = self.load_session(session)?
+ {
+ provider_session.unread_count = provider_session.unread_count.saturating_sub(1);
+ self.store.upsert_session(provider_session)?;
+ }
+ self.reload()?;
+ Ok(message)
+ }
+
+ pub fn apply_provider_event(&mut self, event: ProviderEvent) -> Result<&ClawbotSnapshot> {
+ match event {
+ ProviderEvent::RuntimeStateUpdated(state) => {
+ self.store.upsert_runtime_state(state)?;
+ }
+ ProviderEvent::SessionUpserted(mut session) => {
+ session.bound_thread_id = self.lookup_bound_thread_id(&session.session_ref())?;
+ session.unread_count = self.unread_count_for_session(&session.session_ref())?;
+ if session.bound_thread_id.is_some() {
+ session.status = SessionStatus::Bound;
+ }
+ self.store.upsert_session(session)?;
+ }
+ ProviderEvent::SessionRemoved(session) => {
+ self.store.remove_session(&session)?;
+ }
+ ProviderEvent::InboundMessage(message) => {
+ if self
+ .store
+ .has_inbound_receipt(&message.session, &message.message_id)?
+ {
+ return self.reload();
+ }
+
+ self.store.append_unread_message(&CachedUnreadMessage {
+ provider: message.session.provider,
+ session_id: message.session.session_id.clone(),
+ message_id: message.message_id.clone(),
+ text: message.text,
+ received_at: message.received_at,
+ })?;
+ self.store.record_inbound_receipt(InboundMessageReceipt {
+ provider: message.session.provider,
+ session_id: message.session.session_id.clone(),
+ message_id: message.message_id,
+ received_at: message.received_at,
+ })?;
+
+ let mut session = self
+ .load_session(&message.session)?
+ .unwrap_or(ProviderSession {
+ provider: message.session.provider,
+ session_id: message.session.session_id.clone(),
+ display_name: None,
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ });
+ session.bound_thread_id = self.lookup_bound_thread_id(&message.session)?;
+ session.unread_count = self.unread_count_for_session(&message.session)?;
+ session.last_message_at = Some(message.received_at);
+ if session.bound_thread_id.is_some() {
+ session.status = SessionStatus::Bound;
+ }
+ self.store.upsert_session(session)?;
+ }
+ }
+ self.reload()
+ }
+
+ fn load_session(&self, session: &ProviderSessionRef) -> Result > {
+ Ok(self
+ .store
+ .load_sessions()?
+ .into_iter()
+ .find(|existing| existing.session_ref() == *session))
+ }
+
+ fn lookup_bound_thread_id(&self, session: &ProviderSessionRef) -> Result > {
+ Ok(self
+ .store
+ .load_bindings()?
+ .into_iter()
+ .find(|binding| binding.session_ref() == *session)
+ .map(|binding| binding.thread_id))
+ }
+
+ fn unread_count_for_session(&self, session: &ProviderSessionRef) -> Result {
+ Ok(self
+ .store
+ .load_unread_messages()?
+ .into_iter()
+ .filter(|message| message.session_ref() == *session)
+ .count())
+ }
+
+ fn reconcile_feishu_sessions(
+ &mut self,
+ discovered_sessions: Vec,
+ ) -> Result<&ClawbotSnapshot> {
+ let discovered_refs = discovered_sessions
+ .iter()
+ .map(ProviderSession::session_ref)
+ .collect::>();
+ let bound_refs = self
+ .store
+ .load_bindings()?
+ .into_iter()
+ .filter(|binding| binding.provider == ProviderKind::Feishu)
+ .map(|binding| binding.session_ref())
+ .collect::>();
+ let unread_refs = self
+ .store
+ .load_unread_messages()?
+ .into_iter()
+ .filter(|message| message.provider == ProviderKind::Feishu)
+ .map(|message| message.session_ref())
+ .collect::>();
+ let sessions = self
+ .store
+ .load_sessions()?
+ .into_iter()
+ .filter(|session| {
+ session.provider != ProviderKind::Feishu
+ || discovered_refs.contains(&session.session_ref())
+ || bound_refs.contains(&session.session_ref())
+ || unread_refs.contains(&session.session_ref())
+ })
+ .collect::>();
+ let unread_messages = self
+ .store
+ .load_unread_messages()?
+ .into_iter()
+ .filter(|message| {
+ message.provider != ProviderKind::Feishu
+ || discovered_refs.contains(&message.session_ref())
+ || bound_refs.contains(&message.session_ref())
+ })
+ .collect::>();
+ self.store.save_sessions(&sessions)?;
+ self.store.save_unread_messages(&unread_messages)?;
+ for session in discovered_sessions {
+ self.apply_provider_event(ProviderEvent::SessionUpserted(session))?;
+ }
+ self.reload()
+ }
+}
+
+fn unix_timestamp_now() -> Result {
+ Ok(SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .context("system clock is before UNIX_EPOCH")?
+ .as_secs() as i64)
+}
+
+#[cfg(test)]
+mod tests {
+ use pretty_assertions::assert_eq;
+ use tempfile::tempdir;
+
+ use super::ClawbotRuntime;
+ use crate::config::ClawbotTurnMode;
+ use crate::config::FeishuConfig;
+ use crate::events::ProviderInboundMessage;
+ use crate::model::CachedUnreadMessage;
+ use crate::model::ConnectionStatus;
+ use crate::model::ForwardingDirection;
+ use crate::model::ForwardingState;
+ use crate::model::ProviderKind;
+ use crate::model::ProviderRuntimeState;
+ use crate::model::ProviderSession;
+ use crate::model::ProviderSessionRef;
+ use crate::model::SessionBinding;
+ use crate::model::SessionStatus;
+ use crate::provider::ProviderEvent;
+
+ #[test]
+ fn take_next_unread_message_is_fifo_per_session() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_1");
+
+ runtime
+ .persist_session(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_1".to_string(),
+ display_name: Some("Alice".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ })
+ .expect("session");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: session.clone(),
+ message_id: "msg_2".to_string(),
+ text: "second".to_string(),
+ received_at: 2,
+ }))
+ .expect("second");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: session.clone(),
+ message_id: "msg_1".to_string(),
+ text: "first".to_string(),
+ received_at: 1,
+ }))
+ .expect("first");
+
+ assert_eq!(
+ runtime
+ .take_next_unread_message(&session)
+ .expect("take first")
+ .expect("message")
+ .message_id,
+ "msg_1"
+ );
+ assert_eq!(
+ runtime
+ .take_next_unread_message(&session)
+ .expect("take second")
+ .expect("message")
+ .message_id,
+ "msg_2"
+ );
+ assert_eq!(
+ runtime
+ .take_next_unread_message(&session)
+ .expect("take none"),
+ None
+ );
+ }
+
+ #[test]
+ fn apply_provider_event_deduplicates_inbound_messages() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_2");
+
+ runtime
+ .apply_provider_event(ProviderEvent::RuntimeStateUpdated(ProviderRuntimeState {
+ provider: ProviderKind::Feishu,
+ connection: ConnectionStatus::Connected,
+ last_error: None,
+ updated_at: Some(1),
+ }))
+ .expect("runtime state");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: session.clone(),
+ message_id: "msg_1".to_string(),
+ text: "hello".to_string(),
+ received_at: 10,
+ }))
+ .expect("first inbound");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session,
+ message_id: "msg_1".to_string(),
+ text: "hello".to_string(),
+ received_at: 10,
+ }))
+ .expect("duplicate inbound");
+
+ assert_eq!(runtime.snapshot().runtime.len(), 1);
+ assert_eq!(runtime.snapshot().unread_message_count, 1);
+ assert_eq!(runtime.snapshot().sessions.len(), 1);
+ assert_eq!(runtime.snapshot().sessions[0].unread_count, 1);
+ }
+
+ #[test]
+ fn update_feishu_config_clears_empty_values() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+
+ runtime
+ .update_feishu_config(Some(FeishuConfig {
+ app_id: "app".to_string(),
+ app_secret: "secret".to_string(),
+ verification_token: Some("token".to_string()),
+ encrypt_key: None,
+ bot_open_id: None,
+ bot_user_id: None,
+ coordination: None,
+ }))
+ .expect("save config");
+ assert_eq!(
+ runtime.snapshot().config.feishu,
+ Some(FeishuConfig {
+ app_id: "app".to_string(),
+ app_secret: "secret".to_string(),
+ verification_token: Some("token".to_string()),
+ encrypt_key: None,
+ bot_open_id: None,
+ bot_user_id: None,
+ coordination: None,
+ })
+ );
+
+ runtime
+ .update_feishu_config(Some(FeishuConfig::default()))
+ .expect("clear config");
+ assert_eq!(runtime.snapshot().config.feishu, None);
+ }
+
+ #[test]
+ fn update_turn_mode_persists_in_config() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+
+ runtime
+ .update_turn_mode(ClawbotTurnMode::NonInteractive)
+ .expect("save turn mode");
+
+ let reloaded = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("reload runtime");
+ assert_eq!(
+ reloaded.snapshot().config.turn_mode,
+ ClawbotTurnMode::NonInteractive
+ );
+ }
+
+ #[test]
+ fn disconnect_thread_clears_binding_and_session_state() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_3");
+
+ runtime
+ .persist_session(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_3".to_string(),
+ display_name: Some("Bob".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ })
+ .expect("session");
+ runtime
+ .connect_session_to_thread(&session, "thread_1".to_string(), None)
+ .expect("bind session");
+
+ assert_eq!(
+ runtime.disconnect_thread("thread_1").expect("disconnect"),
+ Some(session.clone())
+ );
+ assert_eq!(
+ runtime
+ .bound_session_for_thread("thread_1")
+ .expect("bound session"),
+ None
+ );
+ assert_eq!(
+ runtime.snapshot().sessions,
+ vec![ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_3".to_string(),
+ display_name: Some("Bob".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }]
+ );
+ }
+
+ #[test]
+ fn set_forwarding_state_for_thread_updates_binding_flags() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_4");
+
+ runtime
+ .connect_session_to_thread(&session, "thread_2".to_string(), None)
+ .expect("bind session");
+
+ runtime
+ .set_forwarding_state_for_thread(
+ "thread_2",
+ ForwardingDirection::Inbound,
+ ForwardingState::Disabled,
+ )
+ .expect("disable inbound");
+ runtime
+ .set_forwarding_state_for_thread(
+ "thread_2",
+ ForwardingDirection::Outbound,
+ ForwardingState::Disabled,
+ )
+ .expect("disable outbound");
+
+ assert_eq!(
+ runtime.snapshot().bindings[0].clone(),
+ SessionBinding {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_4".to_string(),
+ thread_id: "thread_2".to_string(),
+ owner_primary_thread_id: None,
+ inbound_forwarding_enabled: false,
+ outbound_forwarding_enabled: false,
+ created_at: runtime.snapshot().bindings[0].created_at,
+ updated_at: runtime.snapshot().bindings[0].updated_at,
+ }
+ );
+ }
+
+ #[test]
+ fn clear_unbound_feishu_sessions_removes_unbound_sessions_and_unread_cache() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let bound_session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_bound");
+ let unbound_session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_unbound");
+
+ runtime
+ .connect_session_to_thread(&bound_session, "thread_3".to_string(), None)
+ .expect("bind session");
+ runtime
+ .persist_session(ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_unbound".to_string(),
+ display_name: Some("Unbound".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ })
+ .expect("persist unbound session");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: bound_session.clone(),
+ message_id: "msg_bound".to_string(),
+ text: "bound".to_string(),
+ received_at: 1,
+ }))
+ .expect("bound unread");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: unbound_session,
+ message_id: "msg_unbound".to_string(),
+ text: "unbound".to_string(),
+ received_at: 2,
+ }))
+ .expect("unbound unread");
+
+ runtime
+ .clear_unbound_feishu_sessions()
+ .expect("clear unbound sessions");
+
+ assert_eq!(
+ runtime.snapshot().sessions,
+ vec![ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_bound".to_string(),
+ display_name: None,
+ unread_count: 1,
+ last_message_at: Some(1),
+ status: SessionStatus::Bound,
+ bound_thread_id: Some("thread_3".to_string()),
+ }]
+ );
+ assert_eq!(
+ runtime
+ .store()
+ .load_unread_messages()
+ .expect("unread messages"),
+ vec![CachedUnreadMessage {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_bound".to_string(),
+ message_id: "msg_bound".to_string(),
+ text: "bound".to_string(),
+ received_at: 1,
+ }]
+ );
+ }
+
+ #[test]
+ fn reconcile_feishu_sessions_preserves_missing_bound_sessions_and_unread_cache() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ let stale_session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_stale");
+ let live_session = ProviderSessionRef::new(ProviderKind::Feishu, "chat_live");
+
+ runtime
+ .connect_session_to_thread(&stale_session, "thread_stale".to_string(), None)
+ .expect("bind stale session");
+ runtime
+ .apply_provider_event(ProviderEvent::InboundMessage(ProviderInboundMessage {
+ session: stale_session.clone(),
+ message_id: "msg_stale".to_string(),
+ text: "stale".to_string(),
+ received_at: 1,
+ }))
+ .expect("stale unread");
+ runtime
+ .reconcile_feishu_sessions(vec![ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_live".to_string(),
+ display_name: Some("Live".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ }])
+ .expect("reconcile discovered sessions");
+
+ assert_eq!(
+ runtime.snapshot().bindings,
+ vec![SessionBinding {
+ provider: ProviderKind::Feishu,
+ session_id: stale_session.session_id.clone(),
+ thread_id: "thread_stale".to_string(),
+ owner_primary_thread_id: None,
+ inbound_forwarding_enabled: true,
+ outbound_forwarding_enabled: true,
+ created_at: runtime.snapshot().bindings[0].created_at,
+ updated_at: runtime.snapshot().bindings[0].updated_at,
+ }]
+ );
+ assert_eq!(
+ runtime.snapshot().sessions,
+ vec![
+ ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: live_session.session_id,
+ display_name: Some("Live".to_string()),
+ unread_count: 0,
+ last_message_at: None,
+ status: SessionStatus::Discovered,
+ bound_thread_id: None,
+ },
+ ProviderSession {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_stale".to_string(),
+ display_name: None,
+ unread_count: 1,
+ last_message_at: Some(1),
+ status: SessionStatus::Bound,
+ bound_thread_id: Some("thread_stale".to_string()),
+ }
+ ]
+ );
+ assert_eq!(
+ runtime
+ .store()
+ .load_unread_messages()
+ .expect("unread messages"),
+ vec![CachedUnreadMessage {
+ provider: ProviderKind::Feishu,
+ session_id: "chat_stale".to_string(),
+ message_id: "msg_stale".to_string(),
+ text: "stale".to_string(),
+ received_at: 1,
+ }]
+ );
+ }
+
+ #[test]
+ fn can_bind_feishu_session_accepts_configured_bot_identity() {
+ let tempdir = tempdir().expect("tempdir");
+ let mut runtime = ClawbotRuntime::load(tempdir.path().to_path_buf()).expect("runtime");
+ runtime
+ .update_feishu_config(Some(FeishuConfig {
+ bot_open_id: Some("oc_bot_private".to_string()),
+ ..FeishuConfig::default()
+ }))
+ .expect("update config");
+
+ assert_eq!(
+ runtime
+ .can_bind_feishu_session(&ProviderSessionRef::new(
+ ProviderKind::Feishu,
+ "oc_bot_private",
+ ))
+ .expect("can bind"),
+ true
+ );
+ }
+}
diff --git a/codex-rs/clawbot/src/store.rs b/codex-rs/clawbot/src/store.rs
new file mode 100644
index 000000000..e3ab419ea
--- /dev/null
+++ b/codex-rs/clawbot/src/store.rs
@@ -0,0 +1,410 @@
+use std::fs;
+use std::path::Path;
+use std::path::PathBuf;
+
+use anyhow::Context;
+use anyhow::Result;
+use serde::Serialize;
+use serde::de::DeserializeOwned;
+use toml::Value as TomlValue;
+
+use crate::config::ClawbotConfig;
+use crate::model::CLAWBOT_BINDINGS_RELATIVE_PATH;
+use crate::model::CLAWBOT_CONFIG_RELATIVE_PATH;
+use crate::model::CLAWBOT_INBOUND_RECEIPTS_RELATIVE_PATH;
+use crate::model::CLAWBOT_PENDING_TURNS_RELATIVE_PATH;
+use crate::model::CLAWBOT_RELATIVE_DIR;
+use crate::model::CLAWBOT_RUNTIME_RELATIVE_PATH;
+use crate::model::CLAWBOT_SESSIONS_RELATIVE_PATH;
+use crate::model::CLAWBOT_UNREAD_MESSAGES_RELATIVE_PATH;
+use crate::model::CachedUnreadMessage;
+use crate::model::ClawbotSnapshot;
+use crate::model::InboundMessageReceipt;
+use crate::model::PendingClawbotTurn;
+use crate::model::ProviderKind;
+use crate::model::ProviderRuntimeState;
+use crate::model::ProviderSession;
+use crate::model::ProviderSessionRef;
+use crate::model::SessionBinding;
+
+const MAX_INBOUND_RECEIPTS: usize = 4_096;
+
+#[derive(Debug, Clone)]
+pub struct ClawbotStore {
+ workspace_root: PathBuf,
+}
+
+impl ClawbotStore {
+ pub fn new(workspace_root: impl Into) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ }
+ }
+
+ pub fn workspace_root(&self) -> &Path {
+ &self.workspace_root
+ }
+
+ pub fn root_dir(&self) -> PathBuf {
+ self.workspace_root.join(CLAWBOT_RELATIVE_DIR)
+ }
+
+ pub fn config_path(&self) -> PathBuf {
+ self.workspace_root.join(CLAWBOT_CONFIG_RELATIVE_PATH)
+ }
+
+ pub fn sessions_path(&self) -> PathBuf {
+ self.workspace_root.join(CLAWBOT_SESSIONS_RELATIVE_PATH)
+ }
+
+ pub fn bindings_path(&self) -> PathBuf {
+ self.workspace_root.join(CLAWBOT_BINDINGS_RELATIVE_PATH)
+ }
+
+ pub fn unread_messages_path(&self) -> PathBuf {
+ self.workspace_root
+ .join(CLAWBOT_UNREAD_MESSAGES_RELATIVE_PATH)
+ }
+
+ pub fn runtime_path(&self) -> PathBuf {
+ self.workspace_root.join(CLAWBOT_RUNTIME_RELATIVE_PATH)
+ }
+
+ pub fn pending_turns_path(&self) -> PathBuf {
+ self.workspace_root
+ .join(CLAWBOT_PENDING_TURNS_RELATIVE_PATH)
+ }
+
+ pub fn inbound_receipts_path(&self) -> PathBuf {
+ self.workspace_root
+ .join(CLAWBOT_INBOUND_RECEIPTS_RELATIVE_PATH)
+ }
+
+ pub fn ensure_root_dir(&self) -> Result<()> {
+ fs::create_dir_all(self.root_dir())
+ .with_context(|| format!("failed to create {}", self.root_dir().display()))
+ }
+
+ pub fn load_snapshot(&self) -> Result {
+ let config = self.load_config()?;
+ let mut runtime = self.load_runtime_states()?;
+ if config.feishu.is_none()
+ && runtime
+ .iter()
+ .all(|state| state.provider != ProviderKind::Feishu)
+ {
+ runtime.push(ProviderRuntimeState::unconfigured(ProviderKind::Feishu));
+ }
+ let sessions = self.load_sessions()?;
+ let bindings = self.load_bindings()?;
+ let unread_message_count = self.load_unread_messages()?.len();
+ Ok(ClawbotSnapshot {
+ config,
+ runtime,
+ sessions,
+ bindings,
+ unread_message_count,
+ })
+ }
+
+ pub fn load_config(&self) -> Result {
+ let config_path = self.config_path();
+ if !config_path.exists() {
+ return Ok(ClawbotConfig::default());
+ }
+ let raw = fs::read_to_string(&config_path)
+ .with_context(|| format!("failed to read {}", config_path.display()))?;
+ toml::from_str(&raw).with_context(|| format!("failed to parse {}", config_path.display()))
+ }
+
+ pub fn save_config(&self, config: &ClawbotConfig) -> Result<()> {
+ let rendered = toml::to_string_pretty(config).context("failed to encode config")?;
+ let contents = if rendered.trim().is_empty() {
+ String::new()
+ } else {
+ let normalized = rendered
+ .parse::()
+ .ok()
+ .and_then(|value| toml::to_string_pretty(&value).ok())
+ .unwrap_or(rendered);
+ format!("{normalized}\n")
+ };
+ self.write_string_file(&self.config_path(), &contents)
+ }
+
+ pub fn load_runtime_states(&self) -> Result> {
+ read_optional_json_file(&self.runtime_path())
+ .with_context(|| format!("failed to load {}", self.runtime_path().display()))
+ }
+
+ pub fn save_runtime_states(&self, runtime_states: &[ProviderRuntimeState]) -> Result<()> {
+ let mut sorted = runtime_states.to_vec();
+ sorted.sort_by_key(|state| state.provider.title());
+ self.write_json_file(&self.runtime_path(), &sorted)
+ }
+
+ pub fn upsert_runtime_state(
+ &self,
+ runtime_state: ProviderRuntimeState,
+ ) -> Result> {
+ let mut runtime_states = self.load_runtime_states()?;
+ if let Some(existing) = runtime_states
+ .iter_mut()
+ .find(|state| state.provider == runtime_state.provider)
+ {
+ *existing = runtime_state;
+ } else {
+ runtime_states.push(runtime_state);
+ }
+ self.save_runtime_states(&runtime_states)?;
+ Ok(runtime_states)
+ }
+
+ pub fn load_sessions(&self) -> Result> {
+ read_optional_json_file(&self.sessions_path())
+ .with_context(|| format!("failed to load {}", self.sessions_path().display()))
+ }
+
+ pub fn save_sessions(&self, sessions: &[ProviderSession]) -> Result<()> {
+ let mut sorted = sessions.to_vec();
+ sorted.sort_by(|left, right| {
+ left.provider
+ .title()
+ .cmp(right.provider.title())
+ .then(left.session_id.cmp(&right.session_id))
+ });
+ self.write_json_file(&self.sessions_path(), &sorted)
+ }
+
+ pub fn upsert_session(&self, session: ProviderSession) -> Result> {
+ let mut sessions = self.load_sessions()?;
+ if let Some(existing) = sessions
+ .iter_mut()
+ .find(|existing| existing.session_ref() == session.session_ref())
+ {
+ *existing = session;
+ } else {
+ sessions.push(session);
+ }
+ self.save_sessions(&sessions)?;
+ Ok(sessions)
+ }
+
+ pub fn remove_session(&self, session: &ProviderSessionRef) -> Result> {
+ let mut sessions = self.load_sessions()?;
+ sessions.retain(|existing| existing.session_ref() != *session);
+ self.save_sessions(&sessions)?;
+ Ok(sessions)
+ }
+
+ pub fn load_bindings(&self) -> Result> {
+ read_optional_json_file(&self.bindings_path())
+ .with_context(|| format!("failed to load {}", self.bindings_path().display()))
+ }
+
+ pub fn save_bindings(&self, bindings: &[SessionBinding]) -> Result<()> {
+ let mut sorted = bindings.to_vec();
+ sorted.sort_by(|left, right| {
+ left.provider
+ .title()
+ .cmp(right.provider.title())
+ .then(left.session_id.cmp(&right.session_id))
+ .then(left.thread_id.cmp(&right.thread_id))
+ });
+ self.write_json_file(&self.bindings_path(), &sorted)
+ }
+
+ pub fn upsert_binding(&self, binding: SessionBinding) -> Result> {
+ let mut bindings = self.load_bindings()?;
+ if let Some(existing) = bindings
+ .iter_mut()
+ .find(|existing| existing.session_ref() == binding.session_ref())
+ {
+ *existing = binding;
+ } else {
+ bindings.push(binding);
+ }
+ self.save_bindings(&bindings)?;
+ Ok(bindings)
+ }
+
+ pub fn load_unread_messages(&self) -> Result> {
+ let unread_messages_path = self.unread_messages_path();
+ if !unread_messages_path.exists() {
+ return Ok(Vec::new());
+ }
+
+ let raw = fs::read_to_string(&unread_messages_path)
+ .with_context(|| format!("failed to read {}", unread_messages_path.display()))?;
+ raw.lines()
+ .filter(|line| !line.trim().is_empty())
+ .map(|line| {
+ serde_json::from_str::(line)
+ .with_context(|| format!("failed to parse {}", unread_messages_path.display()))
+ })
+ .collect()
+ }
+
+ pub fn save_unread_messages(&self, unread_messages: &[CachedUnreadMessage]) -> Result<()> {
+ let mut sorted = unread_messages.to_vec();
+ sorted.sort_by(|left, right| {
+ left.provider
+ .title()
+ .cmp(right.provider.title())
+ .then(left.session_id.cmp(&right.session_id))
+ .then(left.received_at.cmp(&right.received_at))
+ .then(left.message_id.cmp(&right.message_id))
+ });
+ sorted.dedup_by(|left, right| {
+ left.provider == right.provider
+ && left.session_id == right.session_id
+ && left.message_id == right.message_id
+ });
+
+ let rendered = if sorted.is_empty() {
+ String::new()
+ } else {
+ let lines = sorted
+ .iter()
+ .map(|message| serde_json::to_string(message).context("failed to encode unread"))
+ .collect::>>()?;
+ format!("{}\n", lines.join("\n"))
+ };
+ self.write_string_file(&self.unread_messages_path(), &rendered)
+ }
+
+ pub fn append_unread_message(&self, message: &CachedUnreadMessage) -> Result<()> {
+ let mut unread_messages = self.load_unread_messages()?;
+ unread_messages.push(message.clone());
+ self.save_unread_messages(&unread_messages)
+ }
+
+ pub fn take_next_unread_message(
+ &self,
+ session: &ProviderSessionRef,
+ ) -> Result> {
+ let mut unread_messages = self.load_unread_messages()?;
+ let Some(index) = unread_messages
+ .iter()
+ .enumerate()
+ .filter(|(_, message)| message.session_ref() == *session)
+ .min_by(|(_, left), (_, right)| {
+ left.received_at
+ .cmp(&right.received_at)
+ .then(left.message_id.cmp(&right.message_id))
+ })
+ .map(|(index, _)| index)
+ else {
+ return Ok(None);
+ };
+ let message = unread_messages.remove(index);
+ self.save_unread_messages(&unread_messages)?;
+ Ok(Some(message))
+ }
+
+ pub fn load_pending_turns(&self) -> Result> {
+ read_optional_json_file(&self.pending_turns_path())
+ .with_context(|| format!("failed to load {}", self.pending_turns_path().display()))
+ }
+
+ pub fn save_pending_turns(&self, pending_turns: &[PendingClawbotTurn]) -> Result<()> {
+ let mut sorted = pending_turns.to_vec();
+ sorted.sort_by(|left, right| {
+ left.thread_id
+ .cmp(&right.thread_id)
+ .then(left.turn_id.cmp(&right.turn_id))
+ .then(left.session.session_id.cmp(&right.session.session_id))
+ .then(left.message_id.cmp(&right.message_id))
+ });
+ sorted.dedup_by(|left, right| {
+ left.thread_id == right.thread_id && left.turn_id == right.turn_id
+ });
+ self.write_json_file(&self.pending_turns_path(), &sorted)
+ }
+
+ pub fn upsert_pending_turn(&self, pending_turn: PendingClawbotTurn) -> Result<()> {
+ let mut pending_turns = self.load_pending_turns()?;
+ pending_turns.retain(|existing| {
+ existing.thread_id != pending_turn.thread_id || existing.turn_id != pending_turn.turn_id
+ });
+ pending_turns.push(pending_turn);
+ self.save_pending_turns(&pending_turns)
+ }
+
+ pub fn remove_pending_turn(
+ &self,
+ thread_id: &str,
+ turn_id: &str,
+ ) -> Result> {
+ let mut pending_turns = self.load_pending_turns()?;
+ let Some(index) = pending_turns
+ .iter()
+ .position(|pending| pending.thread_id == thread_id && pending.turn_id == turn_id)
+ else {
+ return Ok(None);
+ };
+ let pending_turn = pending_turns.remove(index);
+ self.save_pending_turns(&pending_turns)?;
+ Ok(Some(pending_turn))
+ }
+
+ pub fn load_inbound_receipts(&self) -> Result> {
+ read_optional_json_file(&self.inbound_receipts_path())
+ .with_context(|| format!("failed to load {}", self.inbound_receipts_path().display()))
+ }
+
+ pub fn has_inbound_receipt(
+ &self,
+ session: &ProviderSessionRef,
+ message_id: &str,
+ ) -> Result {
+ Ok(self
+ .load_inbound_receipts()?
+ .into_iter()
+ .any(|receipt| receipt.session_ref() == *session && receipt.message_id == message_id))
+ }
+
+ pub fn record_inbound_receipt(&self, receipt: InboundMessageReceipt) -> Result<()> {
+ let mut receipts = self.load_inbound_receipts()?;
+ receipts.retain(|existing| {
+ existing.session_ref() != receipt.session_ref()
+ || existing.message_id != receipt.message_id
+ });
+ receipts.push(receipt);
+ receipts.sort_by(|left, right| {
+ left.received_at
+ .cmp(&right.received_at)
+ .then(left.session_id.cmp(&right.session_id))
+ .then(left.message_id.cmp(&right.message_id))
+ });
+ if receipts.len() > MAX_INBOUND_RECEIPTS {
+ receipts.drain(..receipts.len() - MAX_INBOUND_RECEIPTS);
+ }
+ self.write_json_file(&self.inbound_receipts_path(), &receipts)
+ }
+
+ fn write_json_file(&self, path: &Path, value: &T) -> Result<()>
+ where
+ T: Serialize,
+ {
+ let rendered = serde_json::to_string_pretty(value).context("failed to encode json")?;
+ self.write_string_file(path, &format!("{rendered}\n"))
+ }
+
+ fn write_string_file(&self, path: &Path, contents: &str) -> Result<()> {
+ self.ensure_root_dir()?;
+ fs::write(path, contents).with_context(|| format!("failed to write {}", path.display()))
+ }
+}
+
+fn read_optional_json_file(path: &Path) -> Result>
+where
+ T: DeserializeOwned,
+{
+ if !path.exists() {
+ return Ok(Vec::new());
+ }
+ let raw =
+ fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
+ serde_json::from_str(&raw).with_context(|| format!("failed to parse {}", path.display()))
+}
diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs
index 32fb25ad3..5ed19e7af 100644
--- a/codex-rs/cli/src/main.rs
+++ b/codex-rs/cli/src/main.rs
@@ -30,6 +30,7 @@ use codex_tui::ExitReason;
use codex_tui::UpdateAction;
use codex_utils_cli::CliConfigOverrides;
use owo_colors::OwoColorize;
+use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::PathBuf;
use supports_color::Stream;
@@ -502,6 +503,28 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec anyhow::Result<()> {
+ if matches!(exit_info.exit_reason, ExitReason::RespawnRequested) {
+ let Some(thread_id) = exit_info.thread_id.as_ref() else {
+ anyhow::bail!("cannot respawn Codex: current session has no thread id");
+ };
+ respawn_current_codex_session(
+ arg0_paths,
+ respawn_args,
+ &thread_id.to_string(),
+ exit_info.respawn_with_yolo,
+ )?;
+ return Ok(());
+ }
+
+ handle_app_exit(exit_info)
+}
+
/// Handle the app exit and print the results. Optionally run the update action.
fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
match exit_info.exit_reason {
@@ -509,7 +532,7 @@ fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
eprintln!("ERROR: {message}");
std::process::exit(1);
}
- ExitReason::UserRequested => { /* normal exit */ }
+ ExitReason::UserRequested | ExitReason::RespawnRequested => { /* normal exit */ }
}
let update_action = exit_info.update_action;
@@ -523,6 +546,167 @@ fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
Ok(())
}
+fn respawn_current_codex_session(
+ arg0_paths: &Arg0DispatchPaths,
+ respawn_args: &[OsString],
+ thread_id: &str,
+ respawn_with_yolo: bool,
+) -> anyhow::Result<()> {
+ let Some(exe_path) = arg0_paths.codex_self_exe.as_ref() else {
+ anyhow::bail!("unable to respawn Codex: current executable path is unavailable");
+ };
+
+ let mut command = std::process::Command::new(exe_path);
+ command.args(build_codex_respawn_argv(
+ respawn_args,
+ thread_id,
+ respawn_with_yolo,
+ ));
+
+ #[cfg(unix)]
+ {
+ use std::os::unix::process::CommandExt;
+
+ let error = command.exec();
+ anyhow::bail!(
+ "failed to respawn Codex via {}: {error}",
+ exe_path.display()
+ );
+ }
+
+ #[cfg(not(unix))]
+ {
+ command
+ .stdin(std::process::Stdio::inherit())
+ .stdout(std::process::Stdio::inherit())
+ .stderr(std::process::Stdio::inherit());
+ command.spawn().map_err(|error| {
+ anyhow::anyhow!(
+ "failed to respawn Codex via {}: {error}",
+ exe_path.display()
+ )
+ })?;
+ Ok(())
+ }
+}
+
+fn build_codex_respawn_argv(
+ respawn_args: &[OsString],
+ thread_id: &str,
+ respawn_with_yolo: bool,
+) -> Vec {
+ let mut args = normalize_respawn_mode_args(respawn_args, respawn_with_yolo);
+ args.push("resume".into());
+ args.push(thread_id.into());
+ if respawn_with_yolo {
+ args.push("--yolo".into());
+ }
+ args
+}
+
+fn normalize_respawn_mode_args(args: &[OsString], respawn_with_yolo: bool) -> Vec {
+ let mut normalized = Vec::new();
+ let mut iter = args.iter();
+ while let Some(arg) = iter.next() {
+ let Some(arg_str) = arg.to_str() else {
+ normalized.push(arg.clone());
+ continue;
+ };
+ match arg_str {
+ "--yolo" | "--dangerously-bypass-approvals-and-sandbox" => {}
+ "--ask-for-approval" | "--sandbox" if respawn_with_yolo => {
+ let _ = iter.next();
+ }
+ "--full-auto" if respawn_with_yolo => {}
+ _ => normalized.push(arg.clone()),
+ }
+ }
+ normalized
+}
+
+fn approval_mode_cli_arg_name(value: codex_utils_cli::ApprovalModeCliArg) -> &'static str {
+ match value {
+ codex_utils_cli::ApprovalModeCliArg::Untrusted => "untrusted",
+ codex_utils_cli::ApprovalModeCliArg::OnFailure => "on-failure",
+ codex_utils_cli::ApprovalModeCliArg::OnRequest => "on-request",
+ codex_utils_cli::ApprovalModeCliArg::Never => "never",
+ }
+}
+
+fn sandbox_mode_cli_arg_name(value: codex_utils_cli::SandboxModeCliArg) -> &'static str {
+ match value {
+ codex_utils_cli::SandboxModeCliArg::ReadOnly => "read-only",
+ codex_utils_cli::SandboxModeCliArg::WorkspaceWrite => "workspace-write",
+ codex_utils_cli::SandboxModeCliArg::DangerFullAccess => "danger-full-access",
+ }
+}
+
+fn push_arg_value(args: &mut Vec, flag: &'static str, value: impl Into) {
+ args.push(flag.into());
+ args.push(value.into());
+}
+
+fn extend_tui_cli_respawn_args(args: &mut Vec, interactive: &TuiCli) {
+ if let Some(model) = &interactive.model {
+ push_arg_value(args, "--model", model.clone());
+ }
+ if interactive.oss {
+ args.push("--oss".into());
+ }
+ if let Some(provider) = &interactive.oss_provider {
+ push_arg_value(args, "--local-provider", provider.clone());
+ }
+ if let Some(profile) = &interactive.config_profile {
+ push_arg_value(args, "--profile", profile.clone());
+ }
+ if let Some(sandbox_mode) = interactive.sandbox_mode {
+ push_arg_value(args, "--sandbox", sandbox_mode_cli_arg_name(sandbox_mode));
+ }
+ if let Some(approval_policy) = interactive.approval_policy {
+ push_arg_value(
+ args,
+ "--ask-for-approval",
+ approval_mode_cli_arg_name(approval_policy),
+ );
+ }
+ if interactive.full_auto {
+ args.push("--full-auto".into());
+ }
+ if interactive.dangerously_bypass_approvals_and_sandbox {
+ args.push("--yolo".into());
+ }
+ if let Some(cwd) = &interactive.cwd {
+ push_arg_value(args, "--cd", cwd.as_os_str().to_os_string());
+ }
+ if interactive.web_search {
+ args.push("--search".into());
+ }
+ for dir in &interactive.add_dir {
+ push_arg_value(args, "--add-dir", dir.as_os_str().to_os_string());
+ }
+ if interactive.no_alt_screen {
+ args.push("--no-alt-screen".into());
+ }
+ for raw_override in &interactive.config_overrides.raw_overrides {
+ push_arg_value(args, "-c", raw_override.clone());
+ }
+}
+
+fn build_interactive_respawn_args(
+ remote: &InteractiveRemoteOptions,
+ interactive: &TuiCli,
+) -> Vec {
+ let mut args = Vec::new();
+ if let Some(remote_addr) = &remote.remote {
+ push_arg_value(&mut args, "--remote", remote_addr.clone());
+ }
+ if let Some(env_var) = &remote.remote_auth_token_env {
+ push_arg_value(&mut args, "--remote-auth-token-env", env_var.clone());
+ }
+ extend_tui_cli_respawn_args(&mut args, interactive);
+ args
+}
+
/// Run the update action and print the result.
fn run_update_action(action: UpdateAction) -> anyhow::Result<()> {
println!();
@@ -688,6 +872,13 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
&mut interactive.config_overrides,
root_config_overrides.clone(),
);
+ let respawn_args = build_interactive_respawn_args(
+ &InteractiveRemoteOptions {
+ remote: root_remote.clone(),
+ remote_auth_token_env: root_remote_auth_token_env.clone(),
+ },
+ &interactive,
+ );
let exit_info = run_interactive_tui(
interactive,
root_remote.clone(),
@@ -695,7 +886,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
arg0_paths.clone(),
)
.await?;
- handle_app_exit(exit_info)?;
+ finish_interactive_exit(exit_info, &arg0_paths, &respawn_args)?;
}
Some(Subcommand::Exec(mut exec_cli)) => {
reject_remote_mode_for_subcommand(
@@ -834,16 +1025,21 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
include_non_interactive,
config_overrides,
);
- let exit_info = run_interactive_tui(
- interactive,
- remote.remote.or(root_remote.clone()),
- remote
+ let effective_remote = InteractiveRemoteOptions {
+ remote: remote.remote.or(root_remote.clone()),
+ remote_auth_token_env: remote
.remote_auth_token_env
.or(root_remote_auth_token_env.clone()),
+ };
+ let respawn_args = build_interactive_respawn_args(&effective_remote, &interactive);
+ let exit_info = run_interactive_tui(
+ interactive,
+ effective_remote.remote,
+ effective_remote.remote_auth_token_env,
arg0_paths.clone(),
)
.await?;
- handle_app_exit(exit_info)?;
+ finish_interactive_exit(exit_info, &arg0_paths, &respawn_args)?;
}
Some(Subcommand::Fork(ForkCommand {
session_id,
@@ -860,16 +1056,21 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
all,
config_overrides,
);
- let exit_info = run_interactive_tui(
- interactive,
- remote.remote.or(root_remote.clone()),
- remote
+ let effective_remote = InteractiveRemoteOptions {
+ remote: remote.remote.or(root_remote.clone()),
+ remote_auth_token_env: remote
.remote_auth_token_env
.or(root_remote_auth_token_env.clone()),
+ };
+ let respawn_args = build_interactive_respawn_args(&effective_remote, &interactive);
+ let exit_info = run_interactive_tui(
+ interactive,
+ effective_remote.remote,
+ effective_remote.remote_auth_token_env,
arg0_paths.clone(),
)
.await?;
- handle_app_exit(exit_info)?;
+ finish_interactive_exit(exit_info, &arg0_paths, &respawn_args)?;
}
Some(Subcommand::Login(mut login_cli)) => {
reject_remote_mode_for_subcommand(
@@ -1565,6 +1766,12 @@ mod tests {
use codex_protocol::protocol::TokenUsage;
use pretty_assertions::assert_eq;
+ fn lossy_args(args: &[OsString]) -> Vec {
+ args.iter()
+ .map(|arg| arg.to_string_lossy().into_owned())
+ .collect()
+ }
+
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
@@ -1753,6 +1960,7 @@ mod tests {
.map(Result::unwrap),
thread_name: thread_name.map(str::to_string),
update_action: None,
+ respawn_with_yolo: false,
exit_reason: ExitReason::UserRequested,
}
}
@@ -1764,12 +1972,95 @@ mod tests {
thread_id: None,
thread_name: None,
update_action: None,
+ respawn_with_yolo: false,
exit_reason: ExitReason::UserRequested,
};
let lines = format_exit_messages(exit_info, /*color_enabled*/ false);
assert!(lines.is_empty());
}
+ #[test]
+ fn build_interactive_respawn_args_preserves_effective_session_args() {
+ let cli = MultitoolCli::try_parse_from([
+ "codex",
+ "--remote",
+ "ws://127.0.0.1:4500",
+ "--remote-auth-token-env",
+ "CODEX_AUTH",
+ "--model",
+ "gpt-5",
+ "--profile",
+ "work",
+ "--cd",
+ "/tmp/project",
+ "--search",
+ "--add-dir",
+ "/tmp/extra",
+ "--no-alt-screen",
+ "-c",
+ "foo=1",
+ ])
+ .expect("parse");
+ let MultitoolCli {
+ mut interactive,
+ config_overrides: root_overrides,
+ remote,
+ ..
+ } = cli;
+ prepend_config_flags(&mut interactive.config_overrides, root_overrides);
+
+ let args = build_interactive_respawn_args(&remote, &interactive);
+
+ assert_eq!(
+ lossy_args(&args),
+ vec![
+ "--remote",
+ "ws://127.0.0.1:4500",
+ "--remote-auth-token-env",
+ "CODEX_AUTH",
+ "--model",
+ "gpt-5",
+ "--profile",
+ "work",
+ "--cd",
+ "/tmp/project",
+ "--search",
+ "--add-dir",
+ "/tmp/extra",
+ "--no-alt-screen",
+ "-c",
+ "foo=1",
+ ]
+ );
+ }
+
+ #[test]
+ fn build_codex_respawn_argv_rewrites_mode_flags_for_yolo_respawn() {
+ let respawn_args = vec![
+ OsString::from("--remote"),
+ OsString::from("ws://127.0.0.1:4500"),
+ OsString::from("--sandbox"),
+ OsString::from("workspace-write"),
+ OsString::from("--ask-for-approval"),
+ OsString::from("on-request"),
+ OsString::from("--full-auto"),
+ ];
+
+ let argv =
+ build_codex_respawn_argv(&respawn_args, "thread-123", /*respawn_with_yolo*/ true);
+
+ assert_eq!(
+ lossy_args(&argv),
+ vec![
+ "--remote",
+ "ws://127.0.0.1:4500",
+ "resume",
+ "thread-123",
+ "--yolo",
+ ]
+ );
+ }
+
#[test]
fn format_exit_messages_includes_resume_hint_without_color() {
let exit_info = sample_exit_info(
diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs
index b3576cb3c..dd2d896c1 100644
--- a/codex-rs/config/src/types.rs
+++ b/codex-rs/config/src/types.rs
@@ -530,6 +530,35 @@ pub struct ModelAvailabilityNuxConfig {
pub shown_count: HashMap,
}
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
+#[schemars(deny_unknown_fields)]
+pub struct TuiDisplayPreferences {
+ /// Show MCP/custom tool result bodies in transcript cells.
+ /// Defaults to `true`.
+ #[serde(default = "default_true")]
+ pub show_tool_results: bool,
+
+ /// Show hook lifecycle and hook output entries in the transcript.
+ /// Defaults to `true`.
+ #[serde(default = "default_true")]
+ pub show_hook_output: bool,
+
+ /// Show patch/edit diff summaries in transcript cells.
+ /// Defaults to `true`.
+ #[serde(default = "default_true")]
+ pub show_patch_diffs: bool,
+}
+
+impl Default for TuiDisplayPreferences {
+ fn default() -> Self {
+ Self {
+ show_tool_results: true,
+ show_hook_output: true,
+ show_patch_diffs: true,
+ }
+ }
+}
+
/// Collection of settings that are specific to the TUI.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
@@ -582,6 +611,10 @@ pub struct Tui {
/// Startup tooltip availability NUX state persisted by the TUI.
#[serde(default)]
pub model_availability_nux: ModelAvailabilityNuxConfig,
+
+ /// Transcript visibility preferences that affect only local TUI rendering.
+ #[serde(default)]
+ pub display_preferences: TuiDisplayPreferences,
}
const fn default_true() -> bool {
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index a0d425981..3ba2f6ca4 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -153,7 +153,6 @@ codex-otel = { workspace = true }
codex-test-binary-support = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
-ctor = { workspace = true }
insta = { workspace = true }
maplit = { workspace = true }
opentelemetry = { workspace = true }
diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json
index eb44f5c8f..d8f1132cf 100644
--- a/codex-rs/core/config.schema.json
+++ b/codex-rs/core/config.schema.json
@@ -1912,6 +1912,19 @@
"description": "Enable animations (welcome screen, shimmer effects, spinners). Defaults to `true`.",
"type": "boolean"
},
+ "display_preferences": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/TuiDisplayPreferences"
+ }
+ ],
+ "default": {
+ "show_hook_output": true,
+ "show_patch_diffs": true,
+ "show_tool_results": true
+ },
+ "description": "Transcript visibility preferences that affect only local TUI rendering."
+ },
"model_availability_nux": {
"allOf": [
{
@@ -1977,6 +1990,27 @@
},
"type": "object"
},
+ "TuiDisplayPreferences": {
+ "additionalProperties": false,
+ "properties": {
+ "show_hook_output": {
+ "default": true,
+ "description": "Show hook lifecycle and hook output entries in the transcript. Defaults to `true`.",
+ "type": "boolean"
+ },
+ "show_patch_diffs": {
+ "default": true,
+ "description": "Show patch/edit diff summaries in transcript cells. Defaults to `true`.",
+ "type": "boolean"
+ },
+ "show_tool_results": {
+ "default": true,
+ "description": "Show MCP/custom tool result bodies in transcript cells. Defaults to `true`.",
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
"UriBasedFileOpener": {
"oneOf": [
{
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index bd83c81f0..4927104cb 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -211,6 +211,8 @@ pub struct ModelClient {
pub struct ModelClientSession {
client: ModelClient,
websocket_session: WebsocketSession,
+ prompt_cache_key_override: Option,
+ prompt_cache_retry_generation: u64,
/// Turn state for sticky routing.
///
/// This is an `OnceLock` that stores the turn state value received from the server
@@ -330,6 +332,8 @@ impl ModelClient {
ModelClientSession {
client: self.clone(),
websocket_session: self.take_cached_websocket_session(),
+ prompt_cache_key_override: None,
+ prompt_cache_retry_generation: 0,
turn_state: Arc::new(OnceLock::new()),
}
}
@@ -807,6 +811,15 @@ impl ModelClientSession {
.set_connection_reused(/*connection_reused*/ false);
}
+ pub(crate) fn rotate_prompt_cache_key_for_retry(&mut self) {
+ self.prompt_cache_retry_generation += 1;
+ let conversation_id = self.client.state.conversation_id;
+ let retry_generation = self.prompt_cache_retry_generation;
+ self.prompt_cache_key_override =
+ Some(format!("{conversation_id}:stream-retry:{retry_generation}"));
+ self.reset_websocket_session();
+ }
+
fn build_responses_request(
&self,
provider: &codex_api::Provider,
@@ -852,7 +865,11 @@ impl ModelClientSession {
None
};
let text = create_text_param_for_request(verbosity, &prompt.output_schema);
- let prompt_cache_key = Some(self.client.state.conversation_id.to_string());
+ let prompt_cache_key = Some(
+ self.prompt_cache_key_override
+ .clone()
+ .unwrap_or_else(|| self.client.state.conversation_id.to_string()),
+ );
let request = ResponsesApiRequest {
model: model_info.slug.clone(),
instructions: instructions.clone(),
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 1b81af705..d1f6e0805 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -193,6 +193,16 @@ pub(crate) use self::turn_context::TurnSkillsContext;
#[cfg(test)]
mod rollout_reconstruction_tests;
+fn should_rotate_prompt_cache_key_for_stream_retry(err: &CodexErr) -> bool {
+ matches!(
+ err,
+ CodexErr::Stream(message, _)
+ if message.contains("stream disconnected before completion")
+ || message.contains("stream closed before response.completed")
+ || message.contains("websocket closed by server before response.completed")
+ )
+}
+
#[derive(Debug, PartialEq)]
pub enum SteerInputError {
NoActiveTurn(Vec),
diff --git a/codex-rs/core/src/codex/turn.rs b/codex-rs/core/src/codex/turn.rs
index 84610804a..760eced6a 100644
--- a/codex-rs/core/src/codex/turn.rs
+++ b/codex-rs/core/src/codex/turn.rs
@@ -1093,6 +1093,9 @@ async fn run_sampling_request(
}
if retries < max_retries {
retries += 1;
+ if super::should_rotate_prompt_cache_key_for_stream_retry(&err) {
+ client_session.rotate_prompt_cache_key_for_retry();
+ }
let delay = match &err {
CodexErr::Stream(_, requested_delay) => {
requested_delay.unwrap_or_else(|| backoff(retries))
diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs
index aa5583405..1a52b6e93 100644
--- a/codex-rs/core/src/codex_tests.rs
+++ b/codex-rs/core/src/codex_tests.rs
@@ -169,6 +169,24 @@ fn skill_message(text: &str) -> ResponseItem {
}
}
+#[test]
+fn rotate_prompt_cache_key_for_stream_disconnected_before_completion() {
+ let err = CodexErr::Stream(
+ "stream disconnected before completion: Incomplete response returned, reason: content_filter"
+ .to_string(),
+ None,
+ );
+
+ assert!(should_rotate_prompt_cache_key_for_stream_retry(&err));
+}
+
+#[test]
+fn does_not_rotate_prompt_cache_key_for_other_stream_errors() {
+ let err = CodexErr::Stream("Selected model is at capacity.".to_string(), None);
+
+ assert!(!should_rotate_prompt_cache_key_for_stream_retry(&err));
+}
+
#[tokio::test]
async fn regular_turn_emits_turn_started_without_waiting_for_startup_prewarm() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs
index 5bdf76881..38d362e97 100644
--- a/codex-rs/core/src/config/config_tests.rs
+++ b/codex-rs/core/src/config/config_tests.rs
@@ -43,6 +43,7 @@ use codex_config::types::SandboxWorkspaceWrite;
use codex_config::types::SkillsConfig;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_config::types::Tui;
+use codex_config::types::TuiDisplayPreferences;
use codex_config::types::TuiNotificationSettings;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
@@ -377,6 +378,7 @@ fn config_toml_deserializes_model_availability_nux() {
("gpt-foo".to_string(), 2),
]),
},
+ display_preferences: TuiDisplayPreferences::default(),
}
);
}
@@ -395,6 +397,33 @@ async fn runtime_config_defaults_model_availability_nux() {
cfg.model_availability_nux,
ModelAvailabilityNuxConfig::default()
);
+ assert_eq!(
+ cfg.tui_display_preferences,
+ TuiDisplayPreferences::default()
+ );
+}
+
+#[test]
+fn config_toml_deserializes_tui_display_preferences() {
+ let toml = r#"
+[tui.display_preferences]
+show_tool_results = false
+show_hook_output = false
+show_patch_diffs = false
+"#;
+ let cfg: ConfigToml =
+ toml::from_str(toml).expect("TOML deserialization should succeed for TUI display prefs");
+
+ assert_eq!(
+ cfg.tui
+ .expect("tui config should deserialize")
+ .display_preferences,
+ TuiDisplayPreferences {
+ show_tool_results: false,
+ show_hook_output: false,
+ show_patch_diffs: false,
+ }
+ );
}
#[test]
@@ -1175,6 +1204,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
terminal_title: None,
theme: None,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
+ display_preferences: TuiDisplayPreferences::default(),
}
);
}
@@ -4842,6 +4872,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
+ tui_display_preferences: TuiDisplayPreferences::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tool_suggest: ToolSuggestConfig::default(),
@@ -4992,6 +5023,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
+ tui_display_preferences: TuiDisplayPreferences::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tool_suggest: ToolSuggestConfig::default(),
@@ -5140,6 +5172,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
+ tui_display_preferences: TuiDisplayPreferences::default(),
analytics_enabled: Some(false),
feedback_enabled: true,
tool_suggest: ToolSuggestConfig::default(),
@@ -5273,6 +5306,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
+ tui_display_preferences: TuiDisplayPreferences::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tool_suggest: ToolSuggestConfig::default(),
diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs
index 47a8bdf30..96e41c97c 100644
--- a/codex-rs/core/src/config/mod.rs
+++ b/codex-rs/core/src/config/mod.rs
@@ -44,6 +44,7 @@ use codex_config::types::OtelExporterKind;
use codex_config::types::ShellEnvironmentPolicy;
use codex_config::types::ToolSuggestConfig;
use codex_config::types::ToolSuggestDiscoverable;
+use codex_config::types::TuiDisplayPreferences;
use codex_config::types::TuiNotificationSettings;
use codex_config::types::UriBasedFileOpener;
use codex_config::types::WindowsSandboxModeToml;
@@ -339,6 +340,9 @@ pub struct Config {
/// Persisted startup availability NUX state for model tooltips.
pub model_availability_nux: ModelAvailabilityNuxConfig,
+ /// Transcript visibility preferences that affect only local TUI rendering.
+ pub tui_display_preferences: TuiDisplayPreferences,
+
/// Start the TUI in the specified collaboration mode (plan/default).
/// Controls whether the TUI uses the terminal's alternate screen buffer.
@@ -2210,6 +2214,11 @@ impl Config {
.as_ref()
.map(|t| t.model_availability_nux.clone())
.unwrap_or_default(),
+ tui_display_preferences: cfg
+ .tui
+ .as_ref()
+ .map(|t| t.display_preferences.clone())
+ .unwrap_or_default(),
tui_alternate_screen: cfg
.tui
.as_ref()
diff --git a/codex-rs/core/src/tools/handlers/grep_files.rs b/codex-rs/core/src/tools/handlers/grep_files.rs
new file mode 100644
index 000000000..ddc61d437
--- /dev/null
+++ b/codex-rs/core/src/tools/handlers/grep_files.rs
@@ -0,0 +1,159 @@
+use crate::function_tool::FunctionCallError;
+use crate::tools::context::FunctionToolOutput;
+use crate::tools::context::ToolInvocation;
+use crate::tools::context::ToolPayload;
+use crate::tools::handlers::parse_arguments;
+use crate::tools::registry::ToolHandler;
+use crate::tools::registry::ToolKind;
+use serde::Deserialize;
+use std::path::Path;
+use std::path::PathBuf;
+use std::process::Stdio;
+use std::time::SystemTime;
+use tokio::process::Command;
+
+pub struct GrepFilesHandler;
+
+const DEFAULT_LIMIT: usize = 100;
+
+fn default_limit() -> usize {
+ DEFAULT_LIMIT
+}
+
+#[derive(Debug, Deserialize)]
+struct GrepFilesArgs {
+ pattern: String,
+ #[serde(default)]
+ include: Option,
+ #[serde(default)]
+ path: Option,
+ #[serde(default = "default_limit")]
+ limit: usize,
+}
+
+impl ToolHandler for GrepFilesHandler {
+ type Output = FunctionToolOutput;
+
+ fn kind(&self) -> ToolKind {
+ ToolKind::Function
+ }
+
+ async fn handle(&self, invocation: ToolInvocation) -> Result {
+ let ToolInvocation { payload, turn, .. } = invocation;
+ let arguments = match payload {
+ ToolPayload::Function { arguments } => arguments,
+ _ => {
+ return Err(FunctionCallError::RespondToModel(
+ "grep_files handler received unsupported payload".to_string(),
+ ));
+ }
+ };
+
+ let args: GrepFilesArgs = parse_arguments(&arguments)?;
+ if args.limit == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "limit must be greater than zero".to_string(),
+ ));
+ }
+
+ let search_root = args
+ .path
+ .map(PathBuf::from)
+ .map(|path| crate::util::resolve_path(turn.cwd.as_path(), &path))
+ .unwrap_or_else(|| turn.cwd.to_path_buf());
+
+ let matches = run_rg_search(
+ &args.pattern,
+ args.include.as_deref(),
+ search_root.as_path(),
+ args.limit,
+ turn.cwd.as_path(),
+ )
+ .await?;
+
+ let output = if matches.is_empty() {
+ "No matching files found".to_string()
+ } else {
+ matches.join("\n")
+ };
+ Ok(FunctionToolOutput::from_text(output, Some(true)))
+ }
+}
+
+async fn run_rg_search(
+ pattern: &str,
+ include: Option<&str>,
+ path: &Path,
+ limit: usize,
+ cwd: &Path,
+) -> Result, FunctionCallError> {
+ let mut command = Command::new("rg");
+ command
+ .arg("--files-with-matches")
+ .arg("--no-messages")
+ .arg("--color")
+ .arg("never")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .current_dir(cwd);
+ if let Some(include) = include {
+ command.arg("--glob").arg(include);
+ }
+ command.arg(pattern).arg(path);
+
+ let output = command
+ .output()
+ .await
+ .map_err(|err| FunctionCallError::RespondToModel(format!("failed to run rg: {err}")))?;
+
+ match output.status.code() {
+ Some(0) => {
+ let mut results = parse_results(&output.stdout, usize::MAX)
+ .into_iter()
+ .map(|result| crate::util::resolve_path(cwd, &PathBuf::from(result)))
+ .collect::>();
+ results.sort_by(|left, right| {
+ let left_modified = modified_time(left.as_path());
+ let right_modified = modified_time(right.as_path());
+ right_modified
+ .cmp(&left_modified)
+ .then_with(|| left.cmp(right))
+ });
+ results.truncate(limit);
+ Ok(results
+ .into_iter()
+ .map(|path| path.display().to_string())
+ .collect())
+ }
+ Some(1) => Ok(Vec::new()),
+ _ => {
+ let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
+ let details = if stderr.is_empty() {
+ format!("rg exited with status {}", output.status)
+ } else {
+ stderr
+ };
+ Err(FunctionCallError::RespondToModel(format!(
+ "grep_files search failed: {details}"
+ )))
+ }
+ }
+}
+
+fn modified_time(path: &Path) -> SystemTime {
+ std::fs::metadata(path)
+ .and_then(|metadata| metadata.modified())
+ .unwrap_or(SystemTime::UNIX_EPOCH)
+}
+
+fn parse_results(stdout: &[u8], limit: usize) -> Vec {
+ String::from_utf8_lossy(stdout)
+ .lines()
+ .take(limit)
+ .map(str::to_string)
+ .collect()
+}
+
+#[cfg(test)]
+#[path = "grep_files_tests.rs"]
+mod tests;
diff --git a/codex-rs/core/src/tools/handlers/grep_files_tests.rs b/codex-rs/core/src/tools/handlers/grep_files_tests.rs
index 0cc247c6f..3d3348616 100644
--- a/codex-rs/core/src/tools/handlers/grep_files_tests.rs
+++ b/codex-rs/core/src/tools/handlers/grep_files_tests.rs
@@ -5,7 +5,7 @@ use tempfile::tempdir;
#[test]
fn parses_basic_results() {
let stdout = b"/tmp/file_a.rs\n/tmp/file_b.rs\n";
- let parsed = parse_results(stdout, 10);
+ let parsed = parse_results(stdout, /*limit*/ 10);
assert_eq!(
parsed,
vec!["/tmp/file_a.rs".to_string(), "/tmp/file_b.rs".to_string()]
@@ -15,7 +15,7 @@ fn parses_basic_results() {
#[test]
fn parse_truncates_after_limit() {
let stdout = b"/tmp/file_a.rs\n/tmp/file_b.rs\n/tmp/file_c.rs\n";
- let parsed = parse_results(stdout, 2);
+ let parsed = parse_results(stdout, /*limit*/ 2);
assert_eq!(
parsed,
vec!["/tmp/file_a.rs".to_string(), "/tmp/file_b.rs".to_string()]
@@ -33,7 +33,7 @@ async fn run_search_returns_results() -> anyhow::Result<()> {
std::fs::write(dir.join("match_two.txt"), "alpha delta").unwrap();
std::fs::write(dir.join("other.txt"), "omega").unwrap();
- let results = run_rg_search("alpha", None, dir, 10, dir).await?;
+ let results = run_rg_search("alpha", /*include*/ None, dir, /*limit*/ 10, dir).await?;
assert_eq!(results.len(), 2);
assert!(results.iter().any(|path| path.ends_with("match_one.txt")));
assert!(results.iter().any(|path| path.ends_with("match_two.txt")));
@@ -50,7 +50,7 @@ async fn run_search_with_glob_filter() -> anyhow::Result<()> {
std::fs::write(dir.join("match_one.rs"), "alpha beta gamma").unwrap();
std::fs::write(dir.join("match_two.txt"), "alpha delta").unwrap();
- let results = run_rg_search("alpha", Some("*.rs"), dir, 10, dir).await?;
+ let results = run_rg_search("alpha", Some("*.rs"), dir, /*limit*/ 10, dir).await?;
assert_eq!(results.len(), 1);
assert!(results.iter().all(|path| path.ends_with("match_one.rs")));
Ok(())
@@ -67,7 +67,7 @@ async fn run_search_respects_limit() -> anyhow::Result<()> {
std::fs::write(dir.join("two.txt"), "alpha two").unwrap();
std::fs::write(dir.join("three.txt"), "alpha three").unwrap();
- let results = run_rg_search("alpha", None, dir, 2, dir).await?;
+ let results = run_rg_search("alpha", /*include*/ None, dir, /*limit*/ 2, dir).await?;
assert_eq!(results.len(), 2);
Ok(())
}
@@ -81,7 +81,7 @@ async fn run_search_handles_no_matches() -> anyhow::Result<()> {
let dir = temp.path();
std::fs::write(dir.join("one.txt"), "omega").unwrap();
- let results = run_rg_search("alpha", None, dir, 5, dir).await?;
+ let results = run_rg_search("alpha", /*include*/ None, dir, /*limit*/ 5, dir).await?;
assert!(results.is_empty());
Ok(())
}
diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs
index 6b48680b8..88bcb6a7c 100644
--- a/codex-rs/core/src/tools/handlers/mod.rs
+++ b/codex-rs/core/src/tools/handlers/mod.rs
@@ -1,6 +1,7 @@
pub(crate) mod agent_jobs;
pub(crate) mod apply_patch;
mod dynamic;
+mod grep_files;
mod js_repl;
mod list_dir;
mod mcp;
@@ -9,6 +10,7 @@ pub(crate) mod multi_agents;
pub(crate) mod multi_agents_common;
pub(crate) mod multi_agents_v2;
mod plan;
+mod read_file;
mod request_permissions;
mod request_user_input;
mod shell;
@@ -37,12 +39,14 @@ pub use apply_patch::ApplyPatchHandler;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
pub use dynamic::DynamicToolHandler;
+pub use grep_files::GrepFilesHandler;
pub use js_repl::JsReplHandler;
pub use js_repl::JsReplResetHandler;
pub use list_dir::ListDirHandler;
pub use mcp::McpHandler;
pub use mcp_resource::McpResourceHandler;
pub use plan::PlanHandler;
+pub use read_file::ReadFileHandler;
pub use request_permissions::RequestPermissionsHandler;
pub use request_user_input::RequestUserInputHandler;
pub use shell::ShellCommandHandler;
diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
index 523b1ed35..fc76e32ca 100644
--- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
+++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
@@ -80,6 +80,14 @@ impl ToolHandler for Handler {
.map_err(FunctionCallError::RespondToModel)?;
}
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
+ if let Some(cwd) = resolve_requested_agent_cwd(&turn.cwd, args.cwd.as_deref())? {
+ config.cwd =
+ codex_utils_absolute_path::AbsolutePathBuf::try_from(cwd).map_err(|error| {
+ FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd must be absolute: {error}"
+ ))
+ })?;
+ }
apply_spawn_agent_overrides(&mut config, child_depth);
let result = session
@@ -182,6 +190,7 @@ struct SpawnAgentArgs {
agent_type: Option,
model: Option,
reasoning_effort: Option,
+ cwd: Option,
#[serde(default)]
fork_context: bool,
}
diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs
index 9c2740d48..a6cc6d011 100644
--- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs
+++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs
@@ -24,6 +24,8 @@ use codex_protocol::user_input::UserInput;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
+use std::path::Path;
+use std::path::PathBuf;
/// Minimum wait timeout to prevent tight polling loops from burning CPU.
pub(crate) const MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
@@ -193,6 +195,44 @@ pub(crate) fn parse_collab_input(
}
}
+pub(crate) fn resolve_requested_agent_cwd(
+ parent_cwd: &Path,
+ requested_cwd: Option<&str>,
+) -> Result, FunctionCallError> {
+ let Some(requested_cwd) = requested_cwd else {
+ return Ok(None);
+ };
+
+ let requested_cwd = requested_cwd.trim();
+ if requested_cwd.is_empty() {
+ return Err(FunctionCallError::RespondToModel(
+ "spawn_agent cwd cannot be empty".to_string(),
+ ));
+ }
+
+ let requested_path = PathBuf::from(requested_cwd);
+ let resolved = if requested_path.is_absolute() {
+ requested_path
+ } else {
+ parent_cwd.join(requested_path)
+ };
+
+ if !resolved.exists() {
+ return Err(FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd {} does not exist",
+ resolved.display()
+ )));
+ }
+ if !resolved.is_dir() {
+ return Err(FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd {} is not a directory",
+ resolved.display()
+ )));
+ }
+
+ Ok(Some(resolved))
+}
+
/// Builds the base config snapshot for a newly spawned sub-agent.
///
/// The returned config starts from the parent's effective config and then refreshes the
diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
index 10f41d97e..e3cbadb47 100644
--- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
+++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
@@ -46,6 +46,7 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::TurnCompleteEvent;
use codex_protocol::user_input::UserInput;
+use core_test_support::PathExt;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
use serde::Deserialize;
@@ -637,6 +638,104 @@ async fn spawn_agent_returns_agent_id_without_task_name() {
assert_eq!(success, Some(true));
}
+#[tokio::test]
+async fn spawn_agent_applies_requested_cwd() {
+ #[derive(Debug, Deserialize)]
+ struct SpawnAgentResult {
+ agent_id: String,
+ }
+
+ let (mut session, mut turn) = make_session_and_context().await;
+ let manager = thread_manager();
+ session.services.agent_control = manager.agent_control();
+ let temp_dir = tempfile::tempdir().expect("tempdir");
+ let child_workspace = temp_dir.path().join("worker-a");
+ std::fs::create_dir(&child_workspace).expect("create child workspace");
+ turn.cwd = temp_dir.path().abs();
+
+ let output = SpawnAgentHandler
+ .handle(invocation(
+ Arc::new(session),
+ Arc::new(turn),
+ "spawn_agent",
+ function_payload(json!({
+ "message": "inspect this repo",
+ "cwd": "worker-a"
+ })),
+ ))
+ .await
+ .expect("spawn_agent should succeed");
+ let (content, _) = expect_text_output(output);
+ let result: SpawnAgentResult =
+ serde_json::from_str(&content).expect("spawn_agent result should be json");
+
+ let snapshot = manager
+ .get_thread(parse_agent_id(&result.agent_id))
+ .await
+ .expect("spawned agent thread should exist")
+ .config_snapshot()
+ .await;
+ assert_eq!(snapshot.cwd, child_workspace.abs());
+}
+
+#[tokio::test]
+async fn spawn_agent_rejects_missing_requested_cwd() {
+ let (session, mut turn) = make_session_and_context().await;
+ let temp_dir = tempfile::tempdir().expect("tempdir");
+ turn.cwd = temp_dir.path().abs();
+
+ let invocation = invocation(
+ Arc::new(session),
+ Arc::new(turn),
+ "spawn_agent",
+ function_payload(json!({
+ "message": "inspect this repo",
+ "cwd": "missing"
+ })),
+ );
+ let Err(err) = SpawnAgentHandler.handle(invocation).await else {
+ panic!("missing cwd should be rejected");
+ };
+ assert_eq!(
+ err,
+ FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd {} does not exist",
+ temp_dir.path().join("missing").display()
+ ))
+ );
+}
+
+#[test]
+fn resolve_requested_agent_cwd_rejects_empty_path() {
+ let temp_dir = tempfile::tempdir().expect("tempdir");
+
+ let err = resolve_requested_agent_cwd(temp_dir.path(), Some(" "))
+ .expect_err("empty cwd should be rejected");
+
+ assert_eq!(
+ err,
+ FunctionCallError::RespondToModel("spawn_agent cwd cannot be empty".to_string())
+ );
+}
+
+#[test]
+fn resolve_requested_agent_cwd_rejects_non_directory() {
+ let temp_dir = tempfile::tempdir().expect("tempdir");
+ let file_path = temp_dir.path().join("worker.txt");
+ std::fs::write(&file_path, "hello").expect("write file");
+
+ let err = resolve_requested_agent_cwd(temp_dir.path(), Some("worker.txt"))
+ .expect_err("non-directory cwd should be rejected");
+
+ assert_eq!(
+ err,
+ FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd {} is not a directory",
+ file_path.display()
+ ))
+ );
+}
+
#[tokio::test]
async fn multi_agent_v2_spawn_requires_task_name() {
let (mut session, mut turn) = make_session_and_context().await;
@@ -828,6 +927,70 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
}));
}
+#[tokio::test]
+async fn multi_agent_v2_spawn_applies_requested_cwd() {
+ #[derive(Debug, Deserialize)]
+ struct SpawnAgentResult {
+ task_name: String,
+ }
+
+ let (mut session, mut turn) = make_session_and_context().await;
+ let manager = thread_manager();
+ let root = manager
+ .start_thread((*turn.config).clone())
+ .await
+ .expect("root thread should start");
+ session.services.agent_control = manager.agent_control();
+ session.conversation_id = root.thread_id;
+ let temp_dir = tempfile::tempdir().expect("tempdir");
+ let child_workspace = temp_dir.path().join("worker-b");
+ std::fs::create_dir(&child_workspace).expect("create child workspace");
+ turn.cwd = temp_dir.path().abs();
+ let mut config = (*turn.config).clone();
+ config
+ .features
+ .enable(Feature::MultiAgentV2)
+ .expect("test config should allow feature update");
+ turn.config = Arc::new(config);
+
+ let session = Arc::new(session);
+ let turn = Arc::new(turn);
+ let output = SpawnAgentHandlerV2
+ .handle(invocation(
+ session.clone(),
+ turn.clone(),
+ "spawn_agent",
+ function_payload(json!({
+ "message": "inspect this repo",
+ "task_name": "worker",
+ "cwd": "worker-b"
+ })),
+ ))
+ .await
+ .expect("spawn_agent should succeed");
+ let (content, _) = expect_text_output(output);
+ let result: SpawnAgentResult =
+ serde_json::from_str(&content).expect("spawn_agent result should be json");
+
+ let child_thread_id = session
+ .services
+ .agent_control
+ .resolve_agent_reference(
+ session.conversation_id,
+ &turn.session_source,
+ &result.task_name,
+ )
+ .await
+ .expect("spawned task name should resolve");
+ let snapshot = manager
+ .get_thread(child_thread_id)
+ .await
+ .expect("spawned agent thread should exist")
+ .config_snapshot()
+ .await;
+ assert_eq!(snapshot.cwd, child_workspace.abs());
+}
+
#[tokio::test]
async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
let (mut session, mut turn) = make_session_and_context().await;
diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs
index 03287b20a..51c44a5c5 100644
--- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs
+++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs
@@ -90,6 +90,14 @@ impl ToolHandler for Handler {
.map_err(FunctionCallError::RespondToModel)?;
}
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
+ if let Some(cwd) = resolve_requested_agent_cwd(&turn.cwd, args.cwd.as_deref())? {
+ config.cwd =
+ codex_utils_absolute_path::AbsolutePathBuf::try_from(cwd).map_err(|error| {
+ FunctionCallError::RespondToModel(format!(
+ "spawn_agent cwd must be absolute: {error}"
+ ))
+ })?;
+ }
apply_spawn_agent_overrides(&mut config, child_depth);
config.developer_instructions = Some(
if let Some(existing_instructions) = config.developer_instructions.take() {
@@ -234,6 +242,7 @@ struct SpawnAgentArgs {
agent_type: Option,
model: Option,
reasoning_effort: Option,
+ cwd: Option,
fork_turns: Option,
fork_context: Option,
}
diff --git a/codex-rs/core/src/tools/handlers/read_file.rs b/codex-rs/core/src/tools/handlers/read_file.rs
new file mode 100644
index 000000000..91a0e9a7d
--- /dev/null
+++ b/codex-rs/core/src/tools/handlers/read_file.rs
@@ -0,0 +1,563 @@
+use crate::function_tool::FunctionCallError;
+use crate::tools::context::FunctionToolOutput;
+use crate::tools::context::ToolInvocation;
+use crate::tools::context::ToolPayload;
+use crate::tools::handlers::parse_arguments;
+use crate::tools::registry::ToolHandler;
+use crate::tools::registry::ToolKind;
+use codex_utils_string::take_bytes_at_char_boundary;
+use serde::Deserialize;
+use std::path::Path;
+use std::path::PathBuf;
+use tokio::fs;
+
+pub struct ReadFileHandler;
+
+const DEFAULT_LIMIT: usize = 200;
+const DEFAULT_OFFSET: usize = 1;
+const MAX_LINE_LENGTH: usize = 2_000;
+
+fn default_limit() -> usize {
+ DEFAULT_LIMIT
+}
+
+fn default_offset() -> usize {
+ DEFAULT_OFFSET
+}
+
+fn default_max_levels() -> usize {
+ 1
+}
+
+fn default_include_header() -> bool {
+ true
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+enum ReadFileMode {
+ #[default]
+ Slice,
+ Indentation,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+struct IndentationArgs {
+ #[serde(default)]
+ anchor_line: Option,
+ #[serde(default)]
+ include_siblings: bool,
+ #[serde(default = "default_max_levels")]
+ max_levels: usize,
+ #[serde(default = "default_include_header")]
+ include_header: bool,
+ #[serde(default)]
+ max_lines: Option,
+}
+
+impl Default for IndentationArgs {
+ fn default() -> Self {
+ Self {
+ anchor_line: None,
+ include_siblings: false,
+ max_levels: default_max_levels(),
+ include_header: default_include_header(),
+ max_lines: None,
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct ReadFileArgs {
+ file_path: String,
+ #[serde(default = "default_offset")]
+ offset: usize,
+ #[serde(default = "default_limit")]
+ limit: usize,
+ #[serde(default)]
+ mode: ReadFileMode,
+ #[serde(default)]
+ indentation: Option,
+}
+
+impl ToolHandler for ReadFileHandler {
+ type Output = FunctionToolOutput;
+
+ fn kind(&self) -> ToolKind {
+ ToolKind::Function
+ }
+
+ async fn handle(&self, invocation: ToolInvocation) -> Result {
+ let ToolInvocation { payload, turn, .. } = invocation;
+ let arguments = match payload {
+ ToolPayload::Function { arguments } => arguments,
+ _ => {
+ return Err(FunctionCallError::RespondToModel(
+ "read_file handler received unsupported payload".to_string(),
+ ));
+ }
+ };
+
+ let args: ReadFileArgs = parse_arguments(&arguments)?;
+ validate_offset_limit(args.offset, args.limit)?;
+
+ let path = crate::util::resolve_path(turn.cwd.as_path(), &PathBuf::from(args.file_path));
+ let lines = match args.mode {
+ ReadFileMode::Slice => slice::read(path.as_path(), args.offset, args.limit).await?,
+ ReadFileMode::Indentation => {
+ indentation::read_block(
+ path.as_path(),
+ args.offset,
+ args.limit,
+ args.indentation.unwrap_or_default(),
+ )
+ .await?
+ }
+ };
+
+ Ok(FunctionToolOutput::from_text(lines.join("\n"), Some(true)))
+ }
+}
+
+#[derive(Clone, Debug)]
+struct LineRecord {
+ text: String,
+ indent: usize,
+ is_blank: bool,
+}
+
+impl LineRecord {
+ fn new(text: String) -> Self {
+ let is_blank = text.trim().is_empty();
+ let indent = text
+ .chars()
+ .take_while(|ch| matches!(ch, ' ' | '\t'))
+ .map(|ch| if ch == '\t' { 4 } else { 1 })
+ .sum();
+ Self {
+ text,
+ indent,
+ is_blank,
+ }
+ }
+
+ fn trimmed(&self) -> &str {
+ self.text.trim()
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct BlockRange {
+ start: usize,
+ actual_start: usize,
+ end: usize,
+}
+
+async fn load_lines(path: &Path) -> Result, FunctionCallError> {
+ let bytes = fs::read(path)
+ .await
+ .map_err(|err| FunctionCallError::RespondToModel(format!("failed to read file: {err}")))?;
+ let text = String::from_utf8_lossy(&bytes);
+ let mut lines = text
+ .split('\n')
+ .map(|line| LineRecord::new(line.trim_end_matches('\r').to_string()))
+ .collect::>();
+ if matches!(lines.last(), Some(last) if last.text.is_empty()) {
+ lines.pop();
+ }
+ Ok(lines)
+}
+
+fn validate_offset_limit(offset: usize, limit: usize) -> Result<(), FunctionCallError> {
+ if offset == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "offset must be a 1-indexed line number".to_string(),
+ ));
+ }
+ if limit == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "limit must be greater than zero".to_string(),
+ ));
+ }
+ Ok(())
+}
+
+fn format_output(
+ lines: &[LineRecord],
+ start: usize,
+ end_exclusive: usize,
+ max_lines: usize,
+) -> Vec {
+ lines[start..end_exclusive]
+ .iter()
+ .take(max_lines)
+ .enumerate()
+ .map(|(offset, line)| format_line(start + offset + 1, line.text.as_str()))
+ .collect()
+}
+
+fn format_line(line_number: usize, line: &str) -> String {
+ let truncated = if line.len() > MAX_LINE_LENGTH {
+ take_bytes_at_char_boundary(line, MAX_LINE_LENGTH).to_string()
+ } else {
+ line.to_string()
+ };
+ format!("L{line_number}: {truncated}")
+}
+
+fn resolve_anchor_index(
+ lines: &[LineRecord],
+ anchor_line: usize,
+) -> Result {
+ if anchor_line == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "anchor_line must be a 1-indexed line number".to_string(),
+ ));
+ }
+ let requested = anchor_line - 1;
+ if requested >= lines.len() {
+ return Err(FunctionCallError::RespondToModel(
+ "anchor_line exceeds file length".to_string(),
+ ));
+ }
+ if !lines[requested].is_blank {
+ return Ok(requested);
+ }
+ if let Some(previous) = (0..requested).rev().find(|index| !lines[*index].is_blank) {
+ return Ok(previous);
+ }
+ ((requested + 1)..lines.len())
+ .find(|index| !lines[*index].is_blank)
+ .ok_or_else(|| {
+ FunctionCallError::RespondToModel(
+ "cannot infer indentation from an empty file".to_string(),
+ )
+ })
+}
+
+fn find_ancestor_start(lines: &[LineRecord], anchor_index: usize, levels: usize) -> Option {
+ let mut current_index = anchor_index;
+ let mut current_indent = lines[anchor_index].indent;
+ let mut found = None;
+ for _ in 0..levels {
+ let next = (0..current_index)
+ .rev()
+ .find(|index| !lines[*index].is_blank && lines[*index].indent < current_indent)?;
+ found = Some(next);
+ current_index = next;
+ current_indent = lines[next].indent;
+ }
+ found
+}
+
+fn is_closing_line(line: &str) -> bool {
+ matches!(line.chars().next(), Some('}') | Some(']') | Some(')'))
+}
+
+fn is_header_line(line: &str) -> bool {
+ line.starts_with("//")
+ || line.starts_with("/*")
+ || line.starts_with('*')
+ || line.starts_with("#[")
+ || line.starts_with("#!")
+ || line.starts_with("///")
+ || line.starts_with("//!")
+ || line.starts_with('@')
+}
+
+fn extend_header_upwards(lines: &[LineRecord], actual_start: usize) -> usize {
+ let indent = lines[actual_start].indent;
+ let mut start = actual_start;
+ while start > 0 {
+ let previous = &lines[start - 1];
+ if previous.is_blank || previous.indent != indent || !is_header_line(previous.trimmed()) {
+ break;
+ }
+ start -= 1;
+ }
+ start
+}
+
+fn block_range(
+ lines: &[LineRecord],
+ actual_start: usize,
+ section_end: usize,
+ include_header: bool,
+) -> BlockRange {
+ let start = if include_header {
+ extend_header_upwards(lines, actual_start)
+ } else {
+ actual_start
+ };
+ let end = find_block_end(lines, actual_start, section_end);
+ BlockRange {
+ start,
+ actual_start,
+ end,
+ }
+}
+
+fn find_block_end(lines: &[LineRecord], actual_start: usize, section_end: usize) -> usize {
+ let start_indent = lines[actual_start].indent;
+ let mut saw_nested = false;
+ let mut last_included = actual_start;
+
+ for (index, line) in lines
+ .iter()
+ .enumerate()
+ .take(section_end + 1)
+ .skip(actual_start + 1)
+ {
+ if line.is_blank {
+ last_included = index;
+ continue;
+ }
+ if line.indent > start_indent {
+ saw_nested = true;
+ last_included = index;
+ continue;
+ }
+ if line.indent == start_indent && is_closing_line(line.trimmed()) {
+ return index;
+ }
+ if saw_nested {
+ return last_included;
+ }
+ return actual_start;
+ }
+
+ last_included
+}
+
+fn qualifies_as_block(lines: &[LineRecord], start: usize, section_end: usize) -> bool {
+ let start_indent = lines[start].indent;
+ for line in lines.iter().take(section_end + 1).skip(start + 1) {
+ if line.is_blank {
+ continue;
+ }
+ return line.indent > start_indent;
+ }
+ false
+}
+
+mod slice {
+ use super::FunctionCallError;
+ use super::LineRecord;
+ use super::Path;
+ use super::format_output;
+ use super::load_lines;
+ use super::validate_offset_limit;
+
+ pub(super) async fn read(
+ path: &Path,
+ offset: usize,
+ limit: usize,
+ ) -> Result, FunctionCallError> {
+ let lines = load_lines(path).await?;
+ read_loaded(lines.as_slice(), offset, limit)
+ }
+
+ pub(super) fn read_loaded(
+ lines: &[LineRecord],
+ offset: usize,
+ limit: usize,
+ ) -> Result, FunctionCallError> {
+ validate_offset_limit(offset, limit)?;
+ let start = offset - 1;
+ if start >= lines.len() {
+ return Err(FunctionCallError::RespondToModel(
+ "offset exceeds file length".to_string(),
+ ));
+ }
+ let end = (start + limit).min(lines.len());
+ Ok(format_output(lines, start, end, limit))
+ }
+}
+
+mod indentation {
+ use super::BlockRange;
+ use super::FunctionCallError;
+ use super::IndentationArgs;
+ use super::LineRecord;
+ use super::Path;
+ use super::block_range;
+ use super::find_ancestor_start;
+ use super::format_output;
+ use super::is_header_line;
+ use super::load_lines;
+ use super::qualifies_as_block;
+ use super::resolve_anchor_index;
+ use super::slice;
+ use super::validate_offset_limit;
+
+ pub(super) async fn read_block(
+ path: &Path,
+ offset: usize,
+ limit: usize,
+ options: IndentationArgs,
+ ) -> Result, FunctionCallError> {
+ validate_offset_limit(offset, limit)?;
+ if options.max_levels == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "indentation.max_levels must be greater than zero".to_string(),
+ ));
+ }
+
+ let max_lines = options.max_lines.unwrap_or(limit);
+ if max_lines == 0 {
+ return Err(FunctionCallError::RespondToModel(
+ "indentation.max_lines must be greater than zero".to_string(),
+ ));
+ }
+
+ let lines = load_lines(path).await?;
+ if lines.is_empty() || offset > lines.len() {
+ return Err(FunctionCallError::RespondToModel(
+ "offset exceeds file length".to_string(),
+ ));
+ }
+
+ let anchor_index = resolve_anchor_index(&lines, options.anchor_line.unwrap_or(offset))?;
+ let Some(actual_start) = find_ancestor_start(&lines, anchor_index, options.max_levels)
+ else {
+ return slice::read_loaded(lines.as_slice(), offset, max_lines);
+ };
+
+ let section_end = lines.len() - 1;
+ let mut selection = block_range(&lines, actual_start, section_end, options.include_header);
+ if options.include_siblings
+ && let Some(parent_start) = find_ancestor_start(&lines, actual_start, /*levels*/ 1)
+ {
+ let parent_scope = block_range(
+ &lines,
+ parent_start,
+ section_end,
+ /*include_header*/ false,
+ );
+ selection =
+ expand_with_siblings(&lines, parent_scope, selection, options.include_header);
+ }
+
+ let end_exclusive = (selection.end + 1).min(lines.len());
+ Ok(format_output(
+ &lines,
+ selection.start,
+ end_exclusive,
+ max_lines,
+ ))
+ }
+
+ fn expand_with_siblings(
+ lines: &[LineRecord],
+ parent_scope: BlockRange,
+ selected: BlockRange,
+ include_header: bool,
+ ) -> BlockRange {
+ let items = collect_scope_items(
+ lines,
+ parent_scope,
+ lines[selected.actual_start].indent,
+ include_header,
+ );
+ let Some(position) = items.iter().position(|item| match item {
+ ScopeItem::Block(block) => block.actual_start == selected.actual_start,
+ ScopeItem::Barrier => false,
+ }) else {
+ return selected;
+ };
+
+ let mut start = selected.start;
+ let mut end = selected.end;
+
+ let mut left = position;
+ while left > 0 {
+ match items[left - 1] {
+ ScopeItem::Block(block) => {
+ start = block.start;
+ left -= 1;
+ }
+ ScopeItem::Barrier => break,
+ }
+ }
+
+ let mut right = position;
+ while right + 1 < items.len() {
+ match items[right + 1] {
+ ScopeItem::Block(block) => {
+ end = block.end;
+ right += 1;
+ }
+ ScopeItem::Barrier => break,
+ }
+ }
+
+ BlockRange {
+ start,
+ actual_start: selected.actual_start,
+ end,
+ }
+ }
+
+ fn collect_scope_items(
+ lines: &[LineRecord],
+ parent_scope: BlockRange,
+ indent: usize,
+ include_header: bool,
+ ) -> Vec {
+ let mut items = Vec::new();
+ let mut index = parent_scope.actual_start.saturating_add(1);
+ while index <= parent_scope.end {
+ let line = &lines[index];
+ if line.is_blank || line.indent != indent {
+ index += 1;
+ continue;
+ }
+
+ if is_header_line(line.trimmed()) {
+ let header_start = index;
+ let mut actual_start = index;
+ while actual_start <= parent_scope.end
+ && !lines[actual_start].is_blank
+ && lines[actual_start].indent == indent
+ && is_header_line(lines[actual_start].trimmed())
+ {
+ actual_start += 1;
+ }
+ if actual_start <= parent_scope.end
+ && lines[actual_start].indent == indent
+ && qualifies_as_block(lines, actual_start, parent_scope.end)
+ {
+ let mut block =
+ block_range(lines, actual_start, parent_scope.end, include_header);
+ block.start = header_start;
+ items.push(ScopeItem::Block(block));
+ index = block.end + 1;
+ continue;
+ }
+ items.push(ScopeItem::Barrier);
+ index = actual_start;
+ continue;
+ }
+
+ if qualifies_as_block(lines, index, parent_scope.end) {
+ let block = block_range(lines, index, parent_scope.end, include_header);
+ items.push(ScopeItem::Block(block));
+ index = block.end + 1;
+ } else {
+ items.push(ScopeItem::Barrier);
+ index += 1;
+ }
+ }
+ items
+ }
+
+ #[derive(Clone, Copy, Debug)]
+ enum ScopeItem {
+ Block(BlockRange),
+ Barrier,
+ }
+}
+
+#[cfg(test)]
+#[path = "read_file_tests.rs"]
+mod tests;
diff --git a/codex-rs/core/src/tools/handlers/read_file_tests.rs b/codex-rs/core/src/tools/handlers/read_file_tests.rs
index 3921a9882..79f16d705 100644
--- a/codex-rs/core/src/tools/handlers/read_file_tests.rs
+++ b/codex-rs/core/src/tools/handlers/read_file_tests.rs
@@ -16,7 +16,7 @@ gamma
"
)?;
- let lines = read(temp.path(), 2, 2).await?;
+ let lines = read(temp.path(), /*offset*/ 2, /*limit*/ 2).await?;
assert_eq!(lines, vec!["L2: beta".to_string(), "L3: gamma".to_string()]);
Ok(())
}
@@ -27,7 +27,7 @@ async fn errors_when_offset_exceeds_length() -> anyhow::Result<()> {
use std::io::Write as _;
writeln!(temp, "only")?;
- let err = read(temp.path(), 3, 1)
+ let err = read(temp.path(), /*offset*/ 3, /*limit*/ 1)
.await
.expect_err("offset exceeds length");
assert_eq!(
@@ -43,7 +43,7 @@ async fn reads_non_utf8_lines() -> anyhow::Result<()> {
use std::io::Write as _;
temp.as_file_mut().write_all(b"\xff\xfe\nplain\n")?;
- let lines = read(temp.path(), 1, 2).await?;
+ let lines = read(temp.path(), /*offset*/ 1, /*limit*/ 2).await?;
let expected_first = format!("L1: {}{}", '\u{FFFD}', '\u{FFFD}');
assert_eq!(lines, vec![expected_first, "L2: plain".to_string()]);
Ok(())
@@ -55,7 +55,7 @@ async fn trims_crlf_endings() -> anyhow::Result<()> {
use std::io::Write as _;
write!(temp, "one\r\ntwo\r\n")?;
- let lines = read(temp.path(), 1, 2).await?;
+ let lines = read(temp.path(), /*offset*/ 1, /*limit*/ 2).await?;
assert_eq!(lines, vec!["L1: one".to_string(), "L2: two".to_string()]);
Ok(())
}
@@ -72,7 +72,7 @@ third
"
)?;
- let lines = read(temp.path(), 1, 2).await?;
+ let lines = read(temp.path(), /*offset*/ 1, /*limit*/ 2).await?;
assert_eq!(
lines,
vec!["L1: first".to_string(), "L2: second".to_string()]
@@ -87,7 +87,7 @@ async fn truncates_lines_longer_than_max_length() -> anyhow::Result<()> {
let long_line = "x".repeat(MAX_LINE_LENGTH + 50);
writeln!(temp, "{long_line}")?;
- let lines = read(temp.path(), 1, 1).await?;
+ let lines = read(temp.path(), /*offset*/ 1, /*limit*/ 1).await?;
let expected = "x".repeat(MAX_LINE_LENGTH);
assert_eq!(lines, vec![format!("L1: {expected}")]);
Ok(())
@@ -115,7 +115,7 @@ async fn indentation_mode_captures_block() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 3, 10, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 3, /*limit*/ 10, options).await?;
assert_eq!(
lines,
@@ -150,7 +150,13 @@ async fn indentation_mode_expands_parents() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 4, 50, options.clone()).await?;
+ let lines = read_block(
+ temp.path(),
+ /*offset*/ 4,
+ /*limit*/ 50,
+ options.clone(),
+ )
+ .await?;
assert_eq!(
lines,
vec![
@@ -163,7 +169,7 @@ async fn indentation_mode_expands_parents() -> anyhow::Result<()> {
);
options.max_levels = 3;
- let expanded = read_block(temp.path(), 4, 50, options).await?;
+ let expanded = read_block(temp.path(), /*offset*/ 4, /*limit*/ 50, options).await?;
assert_eq!(
expanded,
vec![
@@ -203,7 +209,13 @@ async fn indentation_mode_respects_sibling_flag() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 3, 50, options.clone()).await?;
+ let lines = read_block(
+ temp.path(),
+ /*offset*/ 3,
+ /*limit*/ 50,
+ options.clone(),
+ )
+ .await?;
assert_eq!(
lines,
vec![
@@ -214,7 +226,7 @@ async fn indentation_mode_respects_sibling_flag() -> anyhow::Result<()> {
);
options.include_siblings = true;
- let with_siblings = read_block(temp.path(), 3, 50, options).await?;
+ let with_siblings = read_block(temp.path(), /*offset*/ 3, /*limit*/ 50, options).await?;
assert_eq!(
with_siblings,
vec![
@@ -257,7 +269,7 @@ class Bar:
..Default::default()
};
- let lines = read_block(temp.path(), 1, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 1, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
@@ -313,7 +325,7 @@ export function other() {{
..Default::default()
};
- let lines = read_block(temp.path(), 15, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 15, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
@@ -385,7 +397,7 @@ async fn indentation_mode_handles_cpp_sample_shallow() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 18, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 18, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
@@ -413,7 +425,7 @@ async fn indentation_mode_handles_cpp_sample() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 18, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 18, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
@@ -445,7 +457,7 @@ async fn indentation_mode_handles_cpp_sample_no_headers() -> anyhow::Result<()>
..Default::default()
};
- let lines = read_block(temp.path(), 18, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 18, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
@@ -476,7 +488,7 @@ async fn indentation_mode_handles_cpp_sample_siblings() -> anyhow::Result<()> {
..Default::default()
};
- let lines = read_block(temp.path(), 18, 200, options).await?;
+ let lines = read_block(temp.path(), /*offset*/ 18, /*limit*/ 200, options).await?;
assert_eq!(
lines,
vec![
diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs
index 69d222aa8..ee5ac0105 100644
--- a/codex-rs/core/src/tools/handlers/request_user_input.rs
+++ b/codex-rs/core/src/tools/handlers/request_user_input.rs
@@ -7,8 +7,9 @@ use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::protocol::SessionSource;
use codex_protocol::request_user_input::RequestUserInputArgs;
-use codex_tools::REQUEST_USER_INPUT_TOOL_NAME;
-use codex_tools::normalize_request_user_input_args;
+use codex_tools::QUESTION_TOOL_NAME;
+use codex_tools::normalize_request_user_input_args_for_tool;
+use codex_tools::question_unavailable_message;
use codex_tools::request_user_input_unavailable_message;
pub struct RequestUserInputHandler {
@@ -27,6 +28,7 @@ impl ToolHandler for RequestUserInputHandler {
session,
turn,
call_id,
+ tool_name,
payload,
..
} = invocation;
@@ -35,7 +37,7 @@ impl ToolHandler for RequestUserInputHandler {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(format!(
- "{REQUEST_USER_INPUT_TOOL_NAME} handler received unsupported payload"
+ "{tool_name} handler received unsupported payload"
)));
}
};
@@ -47,28 +49,30 @@ impl ToolHandler for RequestUserInputHandler {
}
let mode = session.collaboration_mode().await.mode;
- if let Some(message) =
- request_user_input_unavailable_message(mode, self.default_mode_request_user_input)
- {
+ let unavailable_message = match tool_name.name.as_str() {
+ QUESTION_TOOL_NAME if tool_name.namespace.is_none() => {
+ question_unavailable_message(mode)
+ }
+ _ => request_user_input_unavailable_message(mode, self.default_mode_request_user_input),
+ };
+ if let Some(message) = unavailable_message {
return Err(FunctionCallError::RespondToModel(message));
}
let args: RequestUserInputArgs = parse_arguments(&arguments)?;
- let args =
- normalize_request_user_input_args(args).map_err(FunctionCallError::RespondToModel)?;
+ let args = normalize_request_user_input_args_for_tool(&tool_name.name, args)
+ .map_err(FunctionCallError::RespondToModel)?;
let response = session
.request_user_input(turn.as_ref(), call_id, args)
.await
.ok_or_else(|| {
FunctionCallError::RespondToModel(format!(
- "{REQUEST_USER_INPUT_TOOL_NAME} was cancelled before receiving a response"
+ "{tool_name} was cancelled before receiving a response"
))
})?;
let content = serde_json::to_string(&response).map_err(|err| {
- FunctionCallError::Fatal(format!(
- "failed to serialize {REQUEST_USER_INPUT_TOOL_NAME} response: {err}"
- ))
+ FunctionCallError::Fatal(format!("failed to serialize {tool_name} response: {err}"))
})?;
Ok(FunctionToolOutput::from_text(content, Some(true)))
diff --git a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs
index 1dac095f1..030648034 100644
--- a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs
+++ b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs
@@ -5,6 +5,7 @@ use crate::tools::context::ToolPayload;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SubAgentSource;
+use codex_tools::REQUEST_USER_INPUT_TOOL_NAME;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::sync::Arc;
diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs
index 1a1a9f2e5..a3a50fe1b 100644
--- a/codex-rs/core/src/tools/spec.rs
+++ b/codex-rs/core/src/tools/spec.rs
@@ -80,12 +80,14 @@ pub(crate) fn build_specs_with_discoverable_tools(
use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
use crate::tools::handlers::DynamicToolHandler;
+ use crate::tools::handlers::GrepFilesHandler;
use crate::tools::handlers::JsReplHandler;
use crate::tools::handlers::JsReplResetHandler;
use crate::tools::handlers::ListDirHandler;
use crate::tools::handlers::McpHandler;
use crate::tools::handlers::McpResourceHandler;
use crate::tools::handlers::PlanHandler;
+ use crate::tools::handlers::ReadFileHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
@@ -206,6 +208,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
ToolHandlerKind::FollowupTaskV2 => {
builder.register_handler(handler.name, Arc::new(FollowupTaskHandlerV2));
}
+ ToolHandlerKind::GrepFiles => {
+ builder.register_handler(handler.name, Arc::new(GrepFilesHandler));
+ }
ToolHandlerKind::JsRepl => {
builder.register_handler(handler.name, js_repl_handler.clone());
}
@@ -227,6 +232,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
ToolHandlerKind::Plan => {
builder.register_handler(handler.name, plan_handler.clone());
}
+ ToolHandlerKind::ReadFile => {
+ builder.register_handler(handler.name, Arc::new(ReadFileHandler));
+ }
ToolHandlerKind::RequestPermissions => {
builder.register_handler(handler.name, request_permissions_handler.clone());
}
diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs
index 9de9c81a9..d304ee8e0 100644
--- a/codex-rs/core/src/tools/spec_tests.rs
+++ b/codex-rs/core/src/tools/spec_tests.rs
@@ -414,6 +414,7 @@ async fn test_build_specs_gpt5_codex_default() {
"shell_command",
&[
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -439,6 +440,7 @@ async fn test_build_specs_gpt51_codex_default() {
"shell_command",
&[
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -454,6 +456,39 @@ async fn test_build_specs_gpt51_codex_default() {
.await;
}
+#[tokio::test]
+async fn experimental_read_and_grep_tools_register_handlers() {
+ let config = test_config().await;
+ let mut model_info = construct_model_info_offline("gpt-5-codex", &config);
+ model_info.experimental_supported_tools =
+ vec!["read_file".to_string(), "grep_files".to_string()];
+ let features = Features::with_defaults();
+ let available_models = Vec::new();
+ let tools_config = ToolsConfig::new(&ToolsConfigParams {
+ model_info: &model_info,
+ available_models: &available_models,
+ features: &features,
+ image_generation_tool_auth_allowed: true,
+ web_search_mode: Some(WebSearchMode::Cached),
+ session_source: SessionSource::Cli,
+ sandbox_policy: &SandboxPolicy::DangerFullAccess,
+ windows_sandbox_level: WindowsSandboxLevel::Disabled,
+ });
+
+ let (tools, registry) = build_specs(
+ &tools_config,
+ /*mcp_tools*/ None,
+ /*app_tools*/ None,
+ &[],
+ )
+ .build();
+
+ assert!(tools.iter().any(|tool| tool.name() == "read_file"));
+ assert!(tools.iter().any(|tool| tool.name() == "grep_files"));
+ assert!(registry.has_handler(&ToolName::plain("read_file")));
+ assert!(registry.has_handler(&ToolName::plain("grep_files")));
+}
+
#[tokio::test]
async fn test_build_specs_gpt5_codex_unified_exec_web_search() {
let mut features = Features::with_defaults();
@@ -466,6 +501,7 @@ async fn test_build_specs_gpt5_codex_unified_exec_web_search() {
"exec_command",
"write_stdin",
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -493,6 +529,7 @@ async fn test_build_specs_gpt51_codex_unified_exec_web_search() {
"exec_command",
"write_stdin",
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -518,6 +555,7 @@ async fn test_gpt_5_1_codex_max_defaults() {
"shell_command",
&[
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -543,6 +581,7 @@ async fn test_codex_5_1_mini_defaults() {
"shell_command",
&[
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -568,6 +607,7 @@ async fn test_gpt_5_defaults() {
"shell",
&[
"update_plan",
+ "question",
"request_user_input",
"web_search",
"image_generation",
@@ -592,6 +632,7 @@ async fn test_gpt_5_1_defaults() {
"shell_command",
&[
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
@@ -619,6 +660,7 @@ async fn test_gpt_5_1_codex_max_unified_exec_web_search() {
"exec_command",
"write_stdin",
"update_plan",
+ "question",
"request_user_input",
"apply_patch",
"web_search",
diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml
index e2765e8be..1ce9456b3 100644
--- a/codex-rs/core/tests/common/Cargo.toml
+++ b/codex-rs/core/tests/common/Cargo.toml
@@ -24,7 +24,6 @@ codex-models-manager = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
-ctor = { workspace = true }
futures = { workspace = true }
notify = { workspace = true }
opentelemetry = { workspace = true }
diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs
index cb899969d..0c74e90c6 100644
--- a/codex-rs/core/tests/common/context_snapshot.rs
+++ b/codex-rs/core/tests/common/context_snapshot.rs
@@ -56,11 +56,13 @@ pub fn format_request_input_snapshot(
request: &ResponsesRequest,
options: &ContextSnapshotOptions,
) -> String {
+ crate::ensure_test_process_initialized();
let items = request.input();
format_response_items_snapshot(items.as_slice(), options)
}
pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshotOptions) -> String {
+ crate::ensure_test_process_initialized();
items
.iter()
.enumerate()
@@ -211,6 +213,7 @@ pub fn format_labeled_requests_snapshot(
sections: &[(&str, &ResponsesRequest)],
options: &ContextSnapshotOptions,
) -> String {
+ crate::ensure_test_process_initialized();
let sections = sections
.iter()
.map(|(title, request)| {
diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs
index a11d5ee6a..cd2acbd20 100644
--- a/codex-rs/core/tests/common/lib.rs
+++ b/codex-rs/core/tests/common/lib.rs
@@ -2,9 +2,9 @@
use anyhow::Context as _;
use anyhow::ensure;
-use codex_arg0::Arg0PathEntryGuard;
use codex_utils_cargo_bin::CargoBinError;
-use ctor::ctor;
+#[cfg(target_os = "linux")]
+use std::os::unix::fs::symlink;
use std::sync::OnceLock;
use tempfile::TempDir;
@@ -28,41 +28,76 @@ pub mod test_codex_exec;
pub mod tracing;
pub mod zsh_fork;
-static TEST_ARG0_PATH_ENTRY: OnceLock> = OnceLock::new();
-
-#[ctor]
-fn enable_deterministic_unified_exec_process_ids_for_tests() {
- codex_core::test_support::set_thread_manager_test_mode(/*enabled*/ true);
- codex_core::test_support::set_deterministic_process_ids(/*enabled*/ true);
+#[cfg(target_os = "linux")]
+struct TestHelperPaths {
+ _temp_dir: TempDir,
+ codex_linux_sandbox_exe: Option,
}
-#[ctor]
-fn configure_arg0_dispatch_for_test_binaries() {
- let _ = TEST_ARG0_PATH_ENTRY.get_or_init(codex_arg0::arg0_dispatch);
+#[cfg(target_os = "linux")]
+static TEST_HELPER_PATHS: OnceLock = OnceLock::new();
+static TEST_PROCESS_INIT: OnceLock<()> = OnceLock::new();
+
+pub(crate) fn ensure_test_process_initialized() {
+ TEST_PROCESS_INIT.get_or_init(|| {
+ codex_core::test_support::set_thread_manager_test_mode(/*enabled*/ true);
+ codex_core::test_support::set_deterministic_process_ids(/*enabled*/ true);
+ #[cfg(target_os = "linux")]
+ let _ = TEST_HELPER_PATHS.get_or_init(init_test_helper_paths);
+
+ if std::env::var_os("INSTA_WORKSPACE_ROOT").is_some() {
+ return;
+ }
+
+ let workspace_root = codex_utils_cargo_bin::repo_root()
+ .ok()
+ .map(|root| root.join("codex-rs"));
+
+ if let Some(workspace_root) = workspace_root
+ && let Ok(workspace_root) = workspace_root.canonicalize()
+ {
+ // Safety: guarded by OnceLock so process-global test setup runs
+ // once before helpers rely on the environment variable.
+ unsafe {
+ std::env::set_var("INSTA_WORKSPACE_ROOT", workspace_root);
+ }
+ }
+ });
}
-#[ctor]
-fn configure_insta_workspace_root_for_snapshot_tests() {
- if std::env::var_os("INSTA_WORKSPACE_ROOT").is_some() {
- return;
+#[cfg(target_os = "linux")]
+fn init_test_helper_paths() -> TestHelperPaths {
+ let direct_sandbox_exe = codex_utils_cargo_bin::cargo_bin("codex-linux-sandbox").ok();
+ if direct_sandbox_exe.is_some() {
+ return TestHelperPaths {
+ _temp_dir: tempfile::Builder::new()
+ .prefix("codex-core-test-helpers")
+ .tempdir()
+ .expect("helper tempdir"),
+ codex_linux_sandbox_exe: direct_sandbox_exe,
+ };
}
- let workspace_root = codex_utils_cargo_bin::repo_root()
+ let temp_dir = tempfile::Builder::new()
+ .prefix("codex-core-test-helpers")
+ .tempdir()
+ .expect("helper tempdir");
+ let codex_linux_sandbox_exe = codex_utils_cargo_bin::cargo_bin("codex-exec")
.ok()
- .map(|root| root.join("codex-rs"));
-
- if let Some(workspace_root) = workspace_root
- && let Ok(workspace_root) = workspace_root.canonicalize()
- {
- // Safety: this ctor runs at process startup before test threads begin.
- unsafe {
- std::env::set_var("INSTA_WORKSPACE_ROOT", workspace_root);
- }
+ .and_then(|codex_exec| {
+ let alias_path = temp_dir.path().join("codex-linux-sandbox");
+ symlink(&codex_exec, &alias_path).ok()?;
+ Some(alias_path)
+ });
+ TestHelperPaths {
+ _temp_dir: temp_dir,
+ codex_linux_sandbox_exe,
}
}
#[track_caller]
pub fn assert_regex_match<'s>(pattern: &str, actual: &'s str) -> regex_lite::Captures<'s> {
+ ensure_test_process_initialized();
let regex = Regex::new(pattern).unwrap_or_else(|err| {
panic!("failed to compile regex {pattern:?}: {err}");
});
@@ -129,6 +164,7 @@ pub fn fetch_dotslash_file(
dotslash_file: &std::path::Path,
dotslash_cache: Option<&std::path::Path>,
) -> anyhow::Result {
+ ensure_test_process_initialized();
let mut command = std::process::Command::new("dotslash");
command.arg("--").arg("fetch").arg(dotslash_file);
if let Some(dotslash_cache) = dotslash_cache {
@@ -164,6 +200,7 @@ pub fn fetch_dotslash_file(
/// temporary directory. Using a per-test directory keeps tests hermetic and
/// avoids clobbering a developer’s real `~/.codex`.
pub async fn load_default_config_for_test(codex_home: &TempDir) -> Config {
+ ensure_test_process_initialized();
ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(default_test_overrides())
@@ -189,18 +226,14 @@ fn default_test_overrides() -> ConfigOverrides {
#[cfg(target_os = "linux")]
pub fn find_codex_linux_sandbox_exe() -> Result {
- if let Some(path) = TEST_ARG0_PATH_ENTRY
+ ensure_test_process_initialized();
+ if let Some(path) = TEST_HELPER_PATHS
.get()
- .and_then(Option::as_ref)
- .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
+ .and_then(|paths| paths.codex_linux_sandbox_exe.clone())
{
return Ok(path);
}
- if let Ok(path) = std::env::current_exe() {
- return Ok(path);
- }
-
codex_utils_cargo_bin::cargo_bin("codex-linux-sandbox")
}
diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs
index a5aba950c..4c596353f 100644
--- a/codex-rs/core/tests/common/test_codex.rs
+++ b/codex-rs/core/tests/common/test_codex.rs
@@ -854,12 +854,19 @@ fn custom_tool_call_output<'a>(bodies: &'a [Value], call_id: &str) -> &'a Value
panic!("custom_tool_call_output {call_id} not found");
}
-fn custom_tool_call_output_text(bodies: &[Value], call_id: &str) -> String {
+fn try_custom_tool_call_output_text(
+ bodies: &[Value],
+ call_id: &str,
+) -> std::result::Result {
let output = custom_tool_call_output(bodies, call_id)
.get("output")
- .unwrap_or_else(|| panic!("custom_tool_call_output {call_id} missing output"));
+ .ok_or_else(|| format!("custom_tool_call_output {call_id} missing output"))?;
output_value_to_text(output)
- .unwrap_or_else(|| panic!("custom_tool_call_output {call_id} missing text output"))
+ .ok_or_else(|| format!("custom_tool_call_output {call_id} missing text output"))
+}
+
+fn custom_tool_call_output_text(bodies: &[Value], call_id: &str) -> String {
+ try_custom_tool_call_output_text(bodies, call_id).unwrap_or_else(|err| panic!("{err}"))
}
fn function_call_output<'a>(bodies: &'a [Value], call_id: &str) -> &'a Value {
@@ -878,6 +885,7 @@ fn function_call_output<'a>(bodies: &'a [Value], call_id: &str) -> &'a Value {
}
pub fn test_codex() -> TestCodexBuilder {
+ crate::ensure_test_process_initialized();
TestCodexBuilder {
config_mutators: vec![],
auth: CodexAuth::from_api_key("dummy"),
@@ -908,8 +916,7 @@ mod tests {
}
#[test]
- #[should_panic(expected = "custom_tool_call_output call-2 missing output")]
- fn custom_tool_call_output_text_panics_when_output_is_missing() {
+ fn custom_tool_call_output_text_reports_missing_output() {
let bodies = vec![json!({
"input": [{
"type": "custom_tool_call_output",
@@ -917,6 +924,9 @@ mod tests {
}]
})];
- let _ = custom_tool_call_output_text(&bodies, "call-2");
+ assert_eq!(
+ try_custom_tool_call_output_text(&bodies, "call-2"),
+ Err("custom_tool_call_output call-2 missing output".to_string())
+ );
}
}
diff --git a/codex-rs/core/tests/common/test_codex_exec.rs b/codex-rs/core/tests/common/test_codex_exec.rs
index ad32bcb02..7500e4f54 100644
--- a/codex-rs/core/tests/common/test_codex_exec.rs
+++ b/codex-rs/core/tests/common/test_codex_exec.rs
@@ -42,6 +42,7 @@ fn toml_string_literal(value: &str) -> String {
}
pub fn test_codex_exec() -> TestCodexExecBuilder {
+ crate::ensure_test_process_initialized();
TestCodexExecBuilder {
home: TempDir::new().expect("create temp home"),
cwd: TempDir::new().expect("create temp cwd"),
diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs
index 9f8389c45..d7a0f67fd 100644
--- a/codex-rs/core/tests/suite/mod.rs
+++ b/codex-rs/core/tests/suite/mod.rs
@@ -1,27 +1,4 @@
// Aggregates all former standalone integration tests as modules.
-use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
-use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
-use codex_test_binary_support::TestBinaryDispatchGuard;
-use codex_test_binary_support::TestBinaryDispatchMode;
-use codex_test_binary_support::configure_test_binary_dispatch;
-use ctor::ctor;
-
-// This code runs before any other tests are run.
-// It allows the test binary to behave like codex and dispatch to apply_patch and codex-linux-sandbox
-// based on the arg0.
-// NOTE: this doesn't work on ARM
-#[ctor]
-pub static CODEX_ALIASES_TEMP_DIR: Option = {
- configure_test_binary_dispatch("codex-core-tests", |exe_name, argv1| {
- if argv1 == Some(CODEX_CORE_APPLY_PATCH_ARG1) {
- return TestBinaryDispatchMode::DispatchArg0Only;
- }
- if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
- return TestBinaryDispatchMode::DispatchArg0Only;
- }
- TestBinaryDispatchMode::InstallAliases
- })
-};
#[cfg(not(target_os = "windows"))]
mod abort_tasks;
diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs
index 8e30b37c2..181a1fe24 100644
--- a/codex-rs/core/tests/suite/request_user_input.rs
+++ b/codex-rs/core/tests/suite/request_user_input.rs
@@ -30,6 +30,9 @@ use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
+const QUESTION_TOOL_NAME: &str = "question";
+const REQUEST_USER_INPUT_TOOL_NAME: &str = "request_user_input";
+
fn call_output(req: &ResponsesRequest, call_id: &str) -> String {
let raw = req.function_call_output(call_id);
assert_eq!(
@@ -74,6 +77,34 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
}
async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
+ interactive_question_round_trip_for_mode(
+ REQUEST_USER_INPUT_TOOL_NAME,
+ mode,
+ multiple_choice_request_args(),
+ /*expect_is_other*/ true,
+ mode == ModeKind::Default,
+ )
+ .await
+}
+
+async fn question_round_trip_for_mode(mode: ModeKind) -> anyhow::Result<()> {
+ interactive_question_round_trip_for_mode(
+ QUESTION_TOOL_NAME,
+ mode,
+ freeform_question_request_args(),
+ /*expect_is_other*/ false,
+ /*enable_default_mode_request_user_input*/ false,
+ )
+ .await
+}
+
+async fn interactive_question_round_trip_for_mode(
+ tool_name: &str,
+ mode: ModeKind,
+ request_args: Value,
+ expect_is_other: bool,
+ enable_default_mode_request_user_input: bool,
+) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -87,36 +118,25 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
..
} = builder
.with_config(move |config| {
- if mode == ModeKind::Default {
- config
- .features
- .enable(Feature::DefaultModeRequestUserInput)
- .expect("test config should allow feature update");
+ if enable_default_mode_request_user_input {
+ assert!(
+ config
+ .features
+ .enable(Feature::DefaultModeRequestUserInput)
+ .is_ok(),
+ "test config should allow feature update"
+ );
}
})
.build(&server)
.await?;
- let call_id = "user-input-call";
- let request_args = json!({
- "questions": [{
- "id": "confirm_path",
- "header": "Confirm",
- "question": "Proceed with the plan?",
- "options": [{
- "label": "Yes (Recommended)",
- "description": "Continue the current plan."
- }, {
- "label": "No",
- "description": "Stop and revisit the approach."
- }]
- }]
- })
- .to_string();
+ let call_id = format!("{tool_name}-call");
+ let request_args = request_args.to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
- ev_function_call(call_id, "request_user_input", &request_args),
+ ev_function_call(&call_id, tool_name, &request_args),
ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, first_response).await;
@@ -163,7 +183,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
.await;
assert_eq!(request.call_id, call_id);
assert_eq!(request.questions.len(), 1);
- assert_eq!(request.questions[0].is_other, true);
+ assert_eq!(request.questions[0].is_other, expect_is_other);
let mut answers = HashMap::new();
answers.insert(
@@ -183,7 +203,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = second_mock.single_request();
- let output_text = call_output(&req, call_id);
+ let output_text = call_output(&req, &call_id);
let output_json: Value = serde_json::from_str(&output_text)?;
assert_eq!(
output_json,
@@ -198,6 +218,43 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
}
async fn assert_request_user_input_rejected(mode_name: &str, build_mode: F) -> anyhow::Result<()>
+where
+ F: FnOnce(String) -> CollaborationMode,
+{
+ assert_interactive_question_rejected(
+ REQUEST_USER_INPUT_TOOL_NAME,
+ mode_name,
+ multiple_choice_request_args(),
+ format!("request_user_input is unavailable in {mode_name} mode"),
+ /*enable_default_mode_request_user_input*/ false,
+ build_mode,
+ )
+ .await
+}
+
+async fn assert_question_rejected(mode_name: &str, build_mode: F) -> anyhow::Result<()>
+where
+ F: FnOnce(String) -> CollaborationMode,
+{
+ assert_interactive_question_rejected(
+ QUESTION_TOOL_NAME,
+ mode_name,
+ freeform_question_request_args(),
+ format!("question is unavailable in {mode_name} mode"),
+ /*enable_default_mode_request_user_input*/ false,
+ build_mode,
+ )
+ .await
+}
+
+async fn assert_interactive_question_rejected(
+ tool_name: &str,
+ mode_name: &str,
+ request_args: Value,
+ expected_output: String,
+ enable_default_mode_request_user_input: bool,
+ build_mode: F,
+) -> anyhow::Result<()>
where
F: FnOnce(String) -> CollaborationMode,
{
@@ -205,35 +262,34 @@ where
let server = start_mock_server().await;
- let mut builder = test_codex();
+ let builder = test_codex();
let TestCodex {
codex,
cwd,
session_configured,
..
- } = builder.build(&server).await?;
+ } = builder
+ .with_config(move |config| {
+ if enable_default_mode_request_user_input {
+ assert!(
+ config
+ .features
+ .enable(Feature::DefaultModeRequestUserInput)
+ .is_ok(),
+ "test config should allow feature update"
+ );
+ }
+ })
+ .build(&server)
+ .await?;
let mode_slug = mode_name.to_lowercase().replace(' ', "-");
- let call_id = format!("user-input-{mode_slug}-call");
- let request_args = json!({
- "questions": [{
- "id": "confirm_path",
- "header": "Confirm",
- "question": "Proceed with the plan?",
- "options": [{
- "label": "Yes (Recommended)",
- "description": "Continue the current plan."
- }, {
- "label": "No",
- "description": "Stop and revisit the approach."
- }]
- }]
- })
- .to_string();
+ let call_id = format!("{tool_name}-{mode_slug}-call");
+ let request_args = request_args.to_string();
let first_response = sse(vec![
ev_response_created("resp-1"),
- ev_function_call(&call_id, "request_user_input", &request_args),
+ ev_function_call(&call_id, tool_name, &request_args),
ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, first_response).await;
@@ -272,14 +328,38 @@ where
let req = second_mock.single_request();
let (output, success) = call_output_content_and_success(&req, &call_id);
assert_eq!(success, None);
- assert_eq!(
- output,
- format!("request_user_input is unavailable in {mode_name} mode")
- );
+ assert_eq!(output, expected_output);
Ok(())
}
+fn multiple_choice_request_args() -> Value {
+ json!({
+ "questions": [{
+ "id": "confirm_path",
+ "header": "Confirm",
+ "question": "Proceed with the plan?",
+ "options": [{
+ "label": "Yes (Recommended)",
+ "description": "Continue the current plan."
+ }, {
+ "label": "No",
+ "description": "Stop and revisit the approach."
+ }]
+ }]
+ })
+}
+
+fn freeform_question_request_args() -> Value {
+ json!({
+ "questions": [{
+ "id": "details",
+ "header": "Details",
+ "question": "What should we keep?"
+ }]
+ })
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn request_user_input_rejected_in_execute_mode_alias() -> anyhow::Result<()> {
assert_request_user_input_rejected("Execute", |model| CollaborationMode {
@@ -323,3 +403,39 @@ async fn request_user_input_rejected_in_pair_mode_alias() -> anyhow::Result<()>
})
.await
}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn question_round_trip_resolves_pending_in_plan_mode() -> anyhow::Result<()> {
+ question_round_trip_for_mode(ModeKind::Plan).await
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn question_round_trip_resolves_pending_in_default_mode() -> anyhow::Result<()> {
+ question_round_trip_for_mode(ModeKind::Default).await
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn question_rejected_in_execute_mode_alias() -> anyhow::Result<()> {
+ assert_question_rejected("Execute", |model| CollaborationMode {
+ mode: ModeKind::Execute,
+ settings: Settings {
+ model,
+ reasoning_effort: None,
+ developer_instructions: None,
+ },
+ })
+ .await
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn question_rejected_in_pair_mode_alias() -> anyhow::Result<()> {
+ assert_question_rejected("Pair Programming", |model| CollaborationMode {
+ mode: ModeKind::PairProgramming,
+ settings: Settings {
+ model,
+ reasoning_effort: None,
+ developer_instructions: None,
+ },
+ })
+ .await
+}
diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs
index 149ba1c53..27ced3307 100644
--- a/codex-rs/core/tests/suite/stream_no_completed.rs
+++ b/codex-rs/core/tests/suite/stream_no_completed.rs
@@ -22,6 +22,26 @@ fn sse_incomplete() -> String {
load_sse_fixture(fixture)
}
+fn sse_incomplete_with_reason(reason: &str) -> String {
+ responses::sse(vec![
+ responses::ev_response_created("resp_incomplete"),
+ responses::ev_message_item_added("msg_incomplete", "partial content"),
+ responses::ev_output_text_delta("continued chunk"),
+ serde_json::json!({
+ "type": "response.incomplete",
+ "response": {
+ "id": "resp_incomplete",
+ "object": "response",
+ "status": "incomplete",
+ "error": null,
+ "incomplete_details": {
+ "reason": reason,
+ }
+ }
+ }),
+ ])
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn retries_on_early_close() {
skip_if_no_network!();
@@ -96,6 +116,92 @@ async fn retries_on_early_close() {
2,
"expected retry after incomplete SSE stream"
);
+ let first_request: serde_json::Value =
+ serde_json::from_slice(&requests[0]).expect("first request should be valid JSON");
+ let second_request: serde_json::Value =
+ serde_json::from_slice(&requests[1]).expect("second request should be valid JSON");
+ assert_ne!(
+ first_request["prompt_cache_key"], second_request["prompt_cache_key"],
+ "expected retry after incomplete stream to rotate prompt_cache_key"
+ );
+
+ server.shutdown().await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn retries_on_stream_disconnected_before_completion() {
+ skip_if_no_network!();
+
+ let incomplete_sse = sse_incomplete_with_reason("content_filter");
+ let completed_sse = responses::sse_completed("resp_ok");
+
+ let (server, _) = start_streaming_sse_server(vec![
+ vec![StreamingSseChunk {
+ gate: None,
+ body: incomplete_sse,
+ }],
+ vec![StreamingSseChunk {
+ gate: None,
+ body: completed_sse,
+ }],
+ ])
+ .await;
+
+ let model_provider = ModelProviderInfo {
+ name: "openai".into(),
+ base_url: Some(format!("{}/v1", server.uri())),
+ env_key: Some("PATH".into()),
+ env_key_instructions: None,
+ experimental_bearer_token: None,
+ auth: None,
+ wire_api: WireApi::Responses,
+ query_params: None,
+ http_headers: None,
+ env_http_headers: None,
+ request_max_retries: Some(0),
+ stream_max_retries: Some(1),
+ stream_idle_timeout_ms: Some(2000),
+ websocket_connect_timeout_ms: None,
+ requires_openai_auth: false,
+ supports_websockets: false,
+ };
+
+ let TestCodex { codex, .. } = test_codex()
+ .with_config(move |config| {
+ config.model_provider = model_provider;
+ })
+ .build_with_streaming_server(&server)
+ .await
+ .unwrap();
+
+ codex
+ .submit(Op::UserInput {
+ items: vec![UserInput::Text {
+ text: "hello".into(),
+ text_elements: Vec::new(),
+ }],
+ final_output_json_schema: None,
+ responsesapi_client_metadata: None,
+ })
+ .await
+ .unwrap();
+
+ wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
+
+ let requests = server.requests().await;
+ assert_eq!(
+ requests.len(),
+ 2,
+ "expected retry after incomplete response stream"
+ );
+ let first_request: serde_json::Value =
+ serde_json::from_slice(&requests[0]).expect("first request should be valid JSON");
+ let second_request: serde_json::Value =
+ serde_json::from_slice(&requests[1]).expect("second request should be valid JSON");
+ assert_ne!(
+ first_request["prompt_cache_key"], second_request["prompt_cache_key"],
+ "expected stream disconnected before completion retry to rotate prompt_cache_key"
+ );
server.shutdown().await;
}
diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml
index bed4a4f2f..8b3d53225 100644
--- a/codex-rs/deny.toml
+++ b/codex-rs/deny.toml
@@ -79,6 +79,7 @@ ignore = [
{ id = "RUSTSEC-2024-0320", reason = "yaml-rust is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
{ id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
{ id = "RUSTSEC-2026-0097", reason = "rand 0.8.5 is pulled in via age v0.11.2/codex-secrets and zbus v4.4.0/keyring; no compatible rand 0.8 fixed release, remove when transitive dependencies move to rand >=0.9.3" },
+ { id = "RUSTSEC-2025-0134", reason = "rustls-pemfile is pulled in only by PR5's clawbot/feishu tail via openlark; upstream stable openlark has not removed it yet, so this advisory is temporarily ignored until that dependency can be upgraded separately" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
diff --git a/codex-rs/models-manager/src/collaboration_mode_presets.rs b/codex-rs/models-manager/src/collaboration_mode_presets.rs
index f67ab91db..5404374c1 100644
--- a/codex-rs/models-manager/src/collaboration_mode_presets.rs
+++ b/codex-rs/models-manager/src/collaboration_mode_presets.rs
@@ -55,7 +55,6 @@ fn default_preset(collaboration_modes_config: CollaborationModesConfig) -> Colla
fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfig) -> String {
let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES);
let request_user_input_availability = request_user_input_availability_message(
- ModeKind::Default,
collaboration_modes_config.default_mode_request_user_input,
);
let asking_questions_guidance = asking_questions_guidance_message(
@@ -86,27 +85,22 @@ fn format_mode_names(modes: &[ModeKind]) -> String {
}
}
-fn request_user_input_availability_message(
- mode: ModeKind,
- default_mode_request_user_input: bool,
-) -> String {
- let mode_name = mode.display_name();
- if mode.allows_request_user_input()
- || (default_mode_request_user_input && mode == ModeKind::Default)
- {
- format!("The `request_user_input` tool is available in {mode_name} mode.")
+fn request_user_input_availability_message(default_mode_request_user_input: bool) -> String {
+ let mode_name = ModeKind::Default.display_name();
+ if default_mode_request_user_input {
+ format!("The `question` and `request_user_input` tools are available in {mode_name} mode.")
} else {
format!(
- "The `request_user_input` tool is unavailable in {mode_name} mode. If you call it while in {mode_name} mode, it will return an error."
+ "The `question` tool is available in {mode_name} mode. The legacy `request_user_input` tool is unavailable in {mode_name} mode and will return an error."
)
}
}
fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> String {
if default_mode_request_user_input {
- "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string()
+ "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `question` tool rather than writing a multiple choice question as a textual assistant message. The legacy `request_user_input` tool is still available in Default mode. Never write a multiple choice question as a textual assistant message.".to_string()
} else {
- "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string()
+ "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `question` tool rather than writing a multiple choice question as a textual assistant message. For a single simple clarification, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string()
}
}
diff --git a/codex-rs/models-manager/src/collaboration_mode_presets_tests.rs b/codex-rs/models-manager/src/collaboration_mode_presets_tests.rs
index 361c51762..cc4542133 100644
--- a/codex-rs/models-manager/src/collaboration_mode_presets_tests.rs
+++ b/codex-rs/models-manager/src/collaboration_mode_presets_tests.rs
@@ -31,12 +31,11 @@ fn default_mode_instructions_replace_mode_names_placeholder() {
let expected_snippet = format!("Known mode names are {known_mode_names}.");
assert!(default_instructions.contains(&expected_snippet));
- let expected_availability_message = request_user_input_availability_message(
- ModeKind::Default,
- /*default_mode_request_user_input*/ true,
- );
+ let expected_availability_message =
+ request_user_input_availability_message(/*default_mode_request_user_input*/ true);
assert!(default_instructions.contains(&expected_availability_message));
- assert!(default_instructions.contains("prefer using the `request_user_input` tool"));
+ assert!(default_instructions.contains("prefer using the `question` tool"));
+ assert!(default_instructions.contains("legacy `request_user_input` tool is still available"));
}
#[test]
@@ -47,6 +46,7 @@ fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() {
.expect("default instructions should be set");
assert!(!default_instructions.contains("prefer using the `request_user_input` tool"));
+ assert!(default_instructions.contains("prefer using the `question` tool"));
assert!(
default_instructions.contains("ask the user directly with a concise plain-text question")
);
diff --git a/codex-rs/protocol/src/error.rs b/codex-rs/protocol/src/error.rs
index f421db8af..d2e2a1553 100644
--- a/codex-rs/protocol/src/error.rs
+++ b/codex-rs/protocol/src/error.rs
@@ -220,6 +220,16 @@ impl CodexErr {
CodexErr::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts {
http_status_code: self.http_status_code_value(),
},
+ CodexErr::UnexpectedStatus(err) => match err.status.as_u16() {
+ 401 | 403 => CodexErrorInfo::Unauthorized,
+ 429 => CodexErrorInfo::ResponseTooManyFailedAttempts {
+ http_status_code: Some(429),
+ },
+ 500..=599 => CodexErrorInfo::ResponseTooManyFailedAttempts {
+ http_status_code: Some(err.status.as_u16()),
+ },
+ _ => CodexErrorInfo::Other,
+ },
CodexErr::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed {
http_status_code: self.http_status_code_value(),
},
diff --git a/codex-rs/protocol/src/error_tests.rs b/codex-rs/protocol/src/error_tests.rs
index 0b1d18972..3ccaf6a06 100644
--- a/codex-rs/protocol/src/error_tests.rs
+++ b/codex-rs/protocol/src/error_tests.rs
@@ -71,6 +71,41 @@ fn server_overloaded_maps_to_protocol() {
);
}
+#[test]
+fn unexpected_status_503_maps_to_response_too_many_failed_attempts() {
+ let err = CodexErr::UnexpectedStatus(UnexpectedResponseError {
+ status: StatusCode::SERVICE_UNAVAILABLE,
+ body: "Service temporarily unavailable".to_string(),
+ url: Some("https://example.com/v1/responses".to_string()),
+ cf_ray: None,
+ request_id: None,
+ identity_authorization_error: None,
+ identity_error_code: None,
+ });
+
+ assert_eq!(
+ err.to_codex_protocol_error(),
+ CodexErrorInfo::ResponseTooManyFailedAttempts {
+ http_status_code: Some(503),
+ }
+ );
+}
+
+#[test]
+fn unexpected_status_401_maps_to_unauthorized() {
+ let err = CodexErr::UnexpectedStatus(UnexpectedResponseError {
+ status: StatusCode::UNAUTHORIZED,
+ body: "Unauthorized".to_string(),
+ url: Some("https://example.com/v1/responses".to_string()),
+ cf_ray: None,
+ request_id: None,
+ identity_authorization_error: None,
+ identity_error_code: None,
+ });
+
+ assert_eq!(err.to_codex_protocol_error(), CodexErrorInfo::Unauthorized);
+}
+
#[test]
fn sandbox_denied_uses_aggregated_output_when_stderr_empty() {
let output = ExecToolCallOutput {
diff --git a/codex-rs/tools/README.md b/codex-rs/tools/README.md
index cafb4b89a..419cd0424 100644
--- a/codex-rs/tools/README.md
+++ b/codex-rs/tools/README.md
@@ -26,7 +26,7 @@ schema and Responses API tool primitives that no longer need to live in
- MCP resource, `list_dir`, and `test_sync_tool` spec builders
- local host tool spec builders for shell/exec/request-permissions/view-image
- collaboration and agent-job `ToolSpec` builders for spawn/send/wait/close,
- `request_user_input`, and CSV fanout/reporting
+ `question`, `request_user_input`, and CSV fanout/reporting
- discoverable-tool models, client filtering, and `ToolSpec` builders for
`tool_search` and `tool_suggest`
- `parse_tool_input_schema()`
diff --git a/codex-rs/tools/src/agent_tool.rs b/codex-rs/tools/src/agent_tool.rs
index aa1699a4e..1e2ebb583 100644
--- a/codex-rs/tools/src/agent_tool.rs
+++ b/codex-rs/tools/src/agent_tool.rs
@@ -538,6 +538,13 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap BTreeMap ToolSpec {
+ create_interactive_question_tool(
+ QUESTION_TOOL_NAME,
+ QuestionToolSchema {
+ questions_description: "Questions to show the user. There is no fixed maximum; use as many as needed for the form.",
+ prompt_description: "Prompt shown to the user for this field.",
+ options_description: "Optional mutually exclusive choices for this question. Omit this field for a freeform text answer. When provided, put the recommended option first and do not include an \"Other\" option; the client can collect additional notes separately.",
+ options_required: false,
+ },
+ description,
+ )
+}
+
pub fn create_request_user_input_tool(description: String) -> ToolSpec {
+ create_interactive_question_tool(
+ REQUEST_USER_INPUT_TOOL_NAME,
+ QuestionToolSchema {
+ questions_description: "Questions to show the user. Prefer 1 and do not exceed 3",
+ prompt_description: "Single-sentence prompt shown to the user.",
+ options_description: "Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; the client will add a free-form \"Other\" option automatically.",
+ options_required: true,
+ },
+ description,
+ )
+}
+
+struct QuestionToolSchema {
+ questions_description: &'static str,
+ prompt_description: &'static str,
+ options_description: &'static str,
+ options_required: bool,
+}
+
+fn create_interactive_question_tool(
+ name: &str,
+ schema: QuestionToolSchema,
+ description: String,
+) -> ToolSpec {
let option_props = BTreeMap::from([
(
"label".to_string(),
@@ -22,14 +66,14 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
),
]);
- let options_schema = JsonSchema::array(JsonSchema::object(
+ let options_schema = JsonSchema::array(
+ JsonSchema::object(
option_props,
Some(vec!["label".to_string(), "description".to_string()]),
Some(false.into()),
- ), Some(
- "Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Do not include an \"Other\" option in this list; the client will add a free-form \"Other\" option automatically."
- .to_string(),
- ));
+ ),
+ Some(schema.options_description.to_string()),
+ );
let question_props = BTreeMap::from([
(
@@ -46,9 +90,7 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
),
(
"question".to_string(),
- JsonSchema::string(Some(
- "Single-sentence prompt shown to the user.".to_string(),
- )),
+ JsonSchema::string(Some(schema.prompt_description.to_string())),
),
("options".to_string(), options_schema),
]);
@@ -56,21 +98,26 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
let questions_schema = JsonSchema::array(
JsonSchema::object(
question_props,
- Some(vec![
- "id".to_string(),
- "header".to_string(),
- "question".to_string(),
- "options".to_string(),
- ]),
+ Some({
+ let mut required = vec![
+ "id".to_string(),
+ "header".to_string(),
+ "question".to_string(),
+ ];
+ if schema.options_required {
+ required.push("options".to_string());
+ }
+ required
+ }),
Some(false.into()),
),
- Some("Questions to show the user. Prefer 1 and do not exceed 3".to_string()),
+ Some(schema.questions_description.to_string()),
);
let properties = BTreeMap::from([("questions".to_string(), questions_schema)]);
ToolSpec::Function(ResponsesApiTool {
- name: REQUEST_USER_INPUT_TOOL_NAME.to_string(),
+ name: name.to_string(),
description,
strict: false,
defer_loading: None,
@@ -83,6 +130,10 @@ pub fn create_request_user_input_tool(description: String) -> ToolSpec {
})
}
+fn question_is_available(mode: ModeKind) -> bool {
+ mode.allows_request_user_input() || mode == ModeKind::Default
+}
+
pub fn request_user_input_unavailable_message(
mode: ModeKind,
default_mode_request_user_input: bool,
@@ -97,28 +148,81 @@ pub fn request_user_input_unavailable_message(
}
}
+pub fn question_unavailable_message(mode: ModeKind) -> Option {
+ if question_is_available(mode) {
+ None
+ } else {
+ let mode_name = mode.display_name();
+ Some(format!("question is unavailable in {mode_name} mode"))
+ }
+}
+
+fn tool_is_available(
+ tool_name: &str,
+ mode: ModeKind,
+ default_mode_request_user_input: bool,
+) -> bool {
+ match tool_name {
+ QUESTION_TOOL_NAME => question_is_available(mode),
+ _ => request_user_input_is_available(mode, default_mode_request_user_input),
+ }
+}
+
+fn question_options_policy(tool_name: &str) -> QuestionOptionsPolicy {
+ match tool_name {
+ QUESTION_TOOL_NAME => QuestionOptionsPolicy::AllowFreeform,
+ _ => QuestionOptionsPolicy::RequireOptions,
+ }
+}
+
pub fn normalize_request_user_input_args(
+ args: RequestUserInputArgs,
+) -> Result {
+ normalize_request_user_input_args_for_tool(REQUEST_USER_INPUT_TOOL_NAME, args)
+}
+
+pub fn normalize_request_user_input_args_for_tool(
+ tool_name: &str,
mut args: RequestUserInputArgs,
) -> Result {
- let missing_options = args
- .questions
- .iter()
- .any(|question| question.options.as_ref().is_none_or(Vec::is_empty));
- if missing_options {
- return Err("request_user_input requires non-empty options for every question".to_string());
+ for question in &mut args.questions {
+ if question.options.as_ref().is_some_and(Vec::is_empty) {
+ question.options = None;
+ }
+ if question
+ .options
+ .as_ref()
+ .is_some_and(|options| !options.is_empty())
+ {
+ question.is_other = true;
+ }
}
- for question in &mut args.questions {
- question.is_other = true;
+ if question_options_policy(tool_name) == QuestionOptionsPolicy::RequireOptions
+ && args
+ .questions
+ .iter()
+ .any(|question| question.options.as_ref().is_none_or(Vec::is_empty))
+ {
+ return Err("request_user_input requires non-empty options for every question".to_string());
}
Ok(args)
}
pub fn request_user_input_tool_description(default_mode_request_user_input: bool) -> String {
- let allowed_modes = format_allowed_modes(default_mode_request_user_input);
- format!(
- "Request user input for one to three short questions and wait for the response. This tool is only available in {allowed_modes}."
+ interactive_question_tool_description(
+ REQUEST_USER_INPUT_TOOL_NAME,
+ "Request user input for one to three short questions and wait for the response.",
+ default_mode_request_user_input,
+ )
+}
+
+pub fn question_tool_description(default_mode_request_user_input: bool) -> String {
+ interactive_question_tool_description(
+ QUESTION_TOOL_NAME,
+ "Ask the user a structured form with as many questions as needed and wait for the response. The client will render choices and/or text fields automatically.",
+ default_mode_request_user_input,
)
}
@@ -127,10 +231,19 @@ fn request_user_input_is_available(mode: ModeKind, default_mode_request_user_inp
|| (default_mode_request_user_input && mode == ModeKind::Default)
}
-fn format_allowed_modes(default_mode_request_user_input: bool) -> String {
+fn interactive_question_tool_description(
+ tool_name: &str,
+ tool_description: &str,
+ default_mode_request_user_input: bool,
+) -> String {
+ let allowed_modes = format_allowed_modes(tool_name, default_mode_request_user_input);
+ format!("{tool_description} This tool is only available in {allowed_modes}.")
+}
+
+fn format_allowed_modes(tool_name: &str, default_mode_request_user_input: bool) -> String {
let mode_names: Vec<&str> = TUI_VISIBLE_COLLABORATION_MODES
.into_iter()
- .filter(|mode| request_user_input_is_available(*mode, default_mode_request_user_input))
+ .filter(|mode| tool_is_available(tool_name, *mode, default_mode_request_user_input))
.map(ModeKind::display_name)
.collect();
diff --git a/codex-rs/tools/src/request_user_input_tool_tests.rs b/codex-rs/tools/src/request_user_input_tool_tests.rs
index 68c2639e4..a3a4700f5 100644
--- a/codex-rs/tools/src/request_user_input_tool_tests.rs
+++ b/codex-rs/tools/src/request_user_input_tool_tests.rs
@@ -1,15 +1,107 @@
use super::*;
use crate::JsonSchema;
use codex_protocol::config_types::ModeKind;
+use codex_protocol::request_user_input::RequestUserInputArgs;
+use codex_protocol::request_user_input::RequestUserInputQuestion;
+use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
+#[test]
+fn question_tool_includes_questions_schema() {
+ assert_eq!(
+ create_question_tool("Ask the user for details.".to_string()),
+ ToolSpec::Function(ResponsesApiTool {
+ name: QUESTION_TOOL_NAME.to_string(),
+ description: "Ask the user for details.".to_string(),
+ strict: false,
+ defer_loading: None,
+ parameters: JsonSchema::object(
+ BTreeMap::from([(
+ "questions".to_string(),
+ JsonSchema::array(
+ JsonSchema::object(
+ BTreeMap::from([
+ (
+ "header".to_string(),
+ JsonSchema::string(Some(
+ "Short header label shown in the UI (12 or fewer chars)."
+ .to_string(),
+ )),
+ ),
+ (
+ "id".to_string(),
+ JsonSchema::string(Some(
+ "Stable identifier for mapping answers (snake_case)."
+ .to_string(),
+ )),
+ ),
+ (
+ "options".to_string(),
+ JsonSchema::array(
+ JsonSchema::object(
+ BTreeMap::from([
+ (
+ "description".to_string(),
+ JsonSchema::string(Some(
+ "One short sentence explaining impact/tradeoff if selected."
+ .to_string(),
+ )),
+ ),
+ (
+ "label".to_string(),
+ JsonSchema::string(Some(
+ "User-facing label (1-5 words)."
+ .to_string(),
+ )),
+ ),
+ ]),
+ Some(vec![
+ "label".to_string(),
+ "description".to_string(),
+ ]),
+ Some(false.into()),
+ ),
+ Some(
+ "Optional mutually exclusive choices for this question. Omit this field for a freeform text answer. When provided, put the recommended option first and do not include an \"Other\" option; the client can collect additional notes separately."
+ .to_string(),
+ ),
+ ),
+ ),
+ (
+ "question".to_string(),
+ JsonSchema::string(Some(
+ "Prompt shown to the user for this field.".to_string(),
+ )),
+ ),
+ ]),
+ Some(vec![
+ "id".to_string(),
+ "header".to_string(),
+ "question".to_string(),
+ ]),
+ Some(false.into()),
+ ),
+ Some(
+ "Questions to show the user. There is no fixed maximum; use as many as needed for the form."
+ .to_string(),
+ ),
+ ),
+ )]),
+ Some(vec!["questions".to_string()]),
+ Some(false.into()),
+ ),
+ output_schema: None,
+ })
+ );
+}
+
#[test]
fn request_user_input_tool_includes_questions_schema() {
assert_eq!(
create_request_user_input_tool("Ask the user to choose.".to_string()),
ToolSpec::Function(ResponsesApiTool {
- name: "request_user_input".to_string(),
+ name: REQUEST_USER_INPUT_TOOL_NAME.to_string(),
description: "Ask the user to choose.".to_string(),
strict: false,
defer_loading: None,
@@ -89,6 +181,20 @@ fn request_user_input_tool_includes_questions_schema() {
);
}
+#[test]
+fn question_unavailable_messages_respect_mode_rules() {
+ assert_eq!(question_unavailable_message(ModeKind::Plan), None);
+ assert_eq!(question_unavailable_message(ModeKind::Default), None);
+ assert_eq!(
+ question_unavailable_message(ModeKind::Execute),
+ Some("question is unavailable in Execute mode".to_string())
+ );
+ assert_eq!(
+ question_unavailable_message(ModeKind::PairProgramming),
+ Some("question is unavailable in Pair Programming mode".to_string())
+ );
+}
+
#[test]
fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
assert_eq!(
@@ -128,6 +234,18 @@ fn request_user_input_unavailable_messages_respect_default_mode_feature_flag() {
);
}
+#[test]
+fn question_tool_description_mentions_available_modes() {
+ assert_eq!(
+ question_tool_description(/*default_mode_request_user_input*/ false),
+ "Ask the user a structured form with as many questions as needed and wait for the response. The client will render choices and/or text fields automatically. This tool is only available in Default or Plan mode.".to_string()
+ );
+ assert_eq!(
+ question_tool_description(/*default_mode_request_user_input*/ true),
+ "Ask the user a structured form with as many questions as needed and wait for the response. The client will render choices and/or text fields automatically. This tool is only available in Default or Plan mode.".to_string()
+ );
+}
+
#[test]
fn request_user_input_tool_description_mentions_available_modes() {
assert_eq!(
@@ -139,3 +257,95 @@ fn request_user_input_tool_description_mentions_available_modes() {
"Request user input for one to three short questions and wait for the response. This tool is only available in Default or Plan mode.".to_string()
);
}
+
+#[test]
+fn normalize_question_args_allows_freeform_questions() {
+ let args = RequestUserInputArgs {
+ questions: vec![RequestUserInputQuestion {
+ id: "details".to_string(),
+ header: "Details".to_string(),
+ question: "What changed?".to_string(),
+ is_other: false,
+ is_secret: false,
+ options: None,
+ }],
+ };
+
+ assert_eq!(
+ normalize_request_user_input_args_for_tool(QUESTION_TOOL_NAME, args.clone()),
+ Ok(args)
+ );
+}
+
+#[test]
+fn normalize_question_args_marks_multiple_choice_entries_as_other() {
+ let args = RequestUserInputArgs {
+ questions: vec![
+ RequestUserInputQuestion {
+ id: "details".to_string(),
+ header: "Details".to_string(),
+ question: "What changed?".to_string(),
+ is_other: false,
+ is_secret: false,
+ options: Some(Vec::new()),
+ },
+ RequestUserInputQuestion {
+ id: "confirm".to_string(),
+ header: "Confirm".to_string(),
+ question: "Continue?".to_string(),
+ is_other: false,
+ is_secret: false,
+ options: Some(vec![RequestUserInputQuestionOption {
+ label: "Yes".to_string(),
+ description: "Keep going.".to_string(),
+ }]),
+ },
+ ],
+ };
+
+ assert_eq!(
+ normalize_request_user_input_args_for_tool(QUESTION_TOOL_NAME, args),
+ Ok(RequestUserInputArgs {
+ questions: vec![
+ RequestUserInputQuestion {
+ id: "details".to_string(),
+ header: "Details".to_string(),
+ question: "What changed?".to_string(),
+ is_other: false,
+ is_secret: false,
+ options: None,
+ },
+ RequestUserInputQuestion {
+ id: "confirm".to_string(),
+ header: "Confirm".to_string(),
+ question: "Continue?".to_string(),
+ is_other: true,
+ is_secret: false,
+ options: Some(vec![RequestUserInputQuestionOption {
+ label: "Yes".to_string(),
+ description: "Keep going.".to_string(),
+ }]),
+ },
+ ],
+ })
+ );
+}
+
+#[test]
+fn normalize_request_user_input_args_requires_options() {
+ let args = RequestUserInputArgs {
+ questions: vec![RequestUserInputQuestion {
+ id: "confirm".to_string(),
+ header: "Confirm".to_string(),
+ question: "Continue?".to_string(),
+ is_other: false,
+ is_secret: false,
+ options: None,
+ }],
+ };
+
+ assert_eq!(
+ normalize_request_user_input_args(args),
+ Err("request_user_input requires non-empty options for every question".to_string())
+ );
+}
diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs
index ac11a2c4b..e4fd28fc4 100644
--- a/codex-rs/tools/src/tool_registry_plan.rs
+++ b/codex-rs/tools/src/tool_registry_plan.rs
@@ -1,4 +1,5 @@
use crate::CommandToolOptions;
+use crate::QUESTION_TOOL_NAME;
use crate::REQUEST_USER_INPUT_TOOL_NAME;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
@@ -25,6 +26,7 @@ use crate::create_close_agent_tool_v2;
use crate::create_code_mode_tool;
use crate::create_exec_command_tool;
use crate::create_followup_task_tool;
+use crate::create_grep_files_tool;
use crate::create_image_generation_tool;
use crate::create_js_repl_reset_tool;
use crate::create_js_repl_tool;
@@ -33,6 +35,8 @@ use crate::create_list_dir_tool;
use crate::create_list_mcp_resource_templates_tool;
use crate::create_list_mcp_resources_tool;
use crate::create_local_shell_tool;
+use crate::create_question_tool;
+use crate::create_read_file_tool;
use crate::create_read_mcp_resource_tool;
use crate::create_report_agent_job_result_tool;
use crate::create_request_permissions_tool;
@@ -58,6 +62,7 @@ use crate::create_write_stdin_tool;
use crate::default_namespace_description;
use crate::dynamic_tool_to_responses_api_tool;
use crate::mcp_tool_to_responses_api_tool;
+use crate::question_tool_description;
use crate::request_permissions_tool_description;
use crate::request_user_input_tool_description;
use crate::tool_registry_plan_types::agent_type_description;
@@ -230,17 +235,27 @@ pub fn build_tool_registry_plan(
plan.register_handler("js_repl_reset", ToolHandlerKind::JsReplReset);
}
- plan.push_spec(
- create_request_user_input_tool(request_user_input_tool_description(
- config.default_mode_request_user_input,
- )),
- /*supports_parallel_tool_calls*/ false,
- config.code_mode_enabled,
- );
- plan.register_handler(
- REQUEST_USER_INPUT_TOOL_NAME,
- ToolHandlerKind::RequestUserInput,
- );
+ if !config.agent_jobs_worker_tools {
+ plan.push_spec(
+ create_question_tool(question_tool_description(
+ config.default_mode_request_user_input,
+ )),
+ /*supports_parallel_tool_calls*/ false,
+ config.code_mode_enabled,
+ );
+ plan.register_handler(QUESTION_TOOL_NAME, ToolHandlerKind::RequestUserInput);
+ plan.push_spec(
+ create_request_user_input_tool(request_user_input_tool_description(
+ config.default_mode_request_user_input,
+ )),
+ /*supports_parallel_tool_calls*/ false,
+ config.code_mode_enabled,
+ );
+ plan.register_handler(
+ REQUEST_USER_INPUT_TOOL_NAME,
+ ToolHandlerKind::RequestUserInput,
+ );
+ }
if config.request_permissions_tool_enabled {
plan.push_spec(
@@ -308,6 +323,34 @@ pub fn build_tool_registry_plan(
plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch);
}
+ if config.has_environment
+ && config
+ .experimental_supported_tools
+ .iter()
+ .any(|tool| tool == "grep_files")
+ {
+ plan.push_spec(
+ create_grep_files_tool(),
+ /*supports_parallel_tool_calls*/ true,
+ config.code_mode_enabled,
+ );
+ plan.register_handler("grep_files", ToolHandlerKind::GrepFiles);
+ }
+
+ if config.has_environment
+ && config
+ .experimental_supported_tools
+ .iter()
+ .any(|tool| tool == "read_file")
+ {
+ plan.push_spec(
+ create_read_file_tool(),
+ /*supports_parallel_tool_calls*/ true,
+ config.code_mode_enabled,
+ );
+ plan.register_handler("read_file", ToolHandlerKind::ReadFile);
+ }
+
if config.has_environment
&& config
.experimental_supported_tools
diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs
index cc8c05dd9..d9401b9a2 100644
--- a/codex-rs/tools/src/tool_registry_plan_tests.rs
+++ b/codex-rs/tools/src/tool_registry_plan_tests.rs
@@ -88,6 +88,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
}),
create_write_stdin_tool(),
create_update_plan_tool(),
+ question_tool_spec(/*default_mode_request_user_input*/ false),
request_user_input_tool_spec(/*default_mode_request_user_input*/ false),
create_apply_patch_freeform_tool(),
ToolSpec::WebSearch {
@@ -183,6 +184,7 @@ fn test_build_specs_collab_tools_enabled() {
};
let (properties, _) = expect_object_schema(parameters);
assert!(properties.contains_key("fork_context"));
+ assert!(properties.contains_key("cwd"));
assert!(!properties.contains_key("fork_turns"));
}
@@ -235,6 +237,7 @@ fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
assert!(properties.contains_key("task_name"));
assert!(properties.contains_key("message"));
assert!(properties.contains_key("fork_turns"));
+ assert!(properties.contains_key("cwd"));
assert!(!properties.contains_key("items"));
assert!(!properties.contains_key("fork_context"));
assert_eq!(
@@ -506,13 +509,14 @@ fn test_build_specs_agent_job_worker_tools_enabled() {
"close_agent",
"spawn_agents_on_csv",
"report_agent_job_result",
- REQUEST_USER_INPUT_TOOL_NAME,
],
);
+ assert_lacks_tool_name(&tools, QUESTION_TOOL_NAME);
+ assert_lacks_tool_name(&tools, "request_user_input");
}
#[test]
-fn request_user_input_description_reflects_default_mode_feature_flag() {
+fn interactive_question_tools_reflect_default_mode_feature_flag() {
let model_info = model_info();
let mut features = Features::with_defaults();
let available_models = Vec::new();
@@ -532,6 +536,11 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
/*deferred_mcp_tools*/ None,
&[],
);
+ let question_tool = find_tool(&tools, QUESTION_TOOL_NAME);
+ assert_eq!(
+ question_tool.spec,
+ question_tool_spec(/*default_mode_request_user_input*/ false)
+ );
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
assert_eq!(
request_user_input_tool.spec,
@@ -555,6 +564,11 @@ fn request_user_input_description_reflects_default_mode_feature_flag() {
/*deferred_mcp_tools*/ None,
&[],
);
+ let question_tool = find_tool(&tools, QUESTION_TOOL_NAME);
+ assert_eq!(
+ question_tool.spec,
+ question_tool_spec(/*default_mode_request_user_input*/ true)
+ );
let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME);
assert_eq!(
request_user_input_tool.spec,
@@ -1060,6 +1074,34 @@ fn test_test_model_info_includes_sync_tool() {
assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool"));
}
+#[test]
+fn test_model_info_includes_read_and_grep_tools() {
+ let mut model_info = model_info();
+ model_info.experimental_supported_tools =
+ vec!["read_file".to_string(), "grep_files".to_string()];
+ let features = Features::with_defaults();
+ let available_models = Vec::new();
+ let tools_config = ToolsConfig::new(&ToolsConfigParams {
+ model_info: &model_info,
+ available_models: &available_models,
+ features: &features,
+ image_generation_tool_auth_allowed: true,
+ web_search_mode: Some(WebSearchMode::Cached),
+ session_source: SessionSource::Cli,
+ sandbox_policy: &SandboxPolicy::DangerFullAccess,
+ windows_sandbox_level: WindowsSandboxLevel::Disabled,
+ });
+ let (tools, _) = build_specs(
+ &tools_config,
+ /*mcp_tools*/ None,
+ /*app_tools*/ None,
+ &[],
+ );
+
+ assert!(tools.iter().any(|tool| tool.name() == "read_file"));
+ assert!(tools.iter().any(|tool| tool.name() == "grep_files"));
+}
+
#[test]
fn test_build_specs_mcp_tools_converted() {
let model_info = model_info();
@@ -2101,6 +2143,10 @@ fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) {
);
}
+fn question_tool_spec(default_mode_request_user_input: bool) -> ToolSpec {
+ create_question_tool(question_tool_description(default_mode_request_user_input))
+}
+
fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec {
create_request_user_input_tool(request_user_input_tool_description(
default_mode_request_user_input,
diff --git a/codex-rs/tools/src/tool_registry_plan_types.rs b/codex-rs/tools/src/tool_registry_plan_types.rs
index b9d66a0c2..deb19e6ad 100644
--- a/codex-rs/tools/src/tool_registry_plan_types.rs
+++ b/codex-rs/tools/src/tool_registry_plan_types.rs
@@ -18,6 +18,7 @@ pub enum ToolHandlerKind {
CodeModeWait,
DynamicTool,
FollowupTaskV2,
+ GrepFiles,
JsRepl,
JsReplReset,
ListAgentsV2,
@@ -25,6 +26,7 @@ pub enum ToolHandlerKind {
Mcp,
McpResource,
Plan,
+ ReadFile,
RequestPermissions,
RequestUserInput,
ResumeAgentV1,
diff --git a/codex-rs/tools/src/utility_tool.rs b/codex-rs/tools/src/utility_tool.rs
index b0f93c972..4cc4585be 100644
--- a/codex-rs/tools/src/utility_tool.rs
+++ b/codex-rs/tools/src/utility_tool.rs
@@ -39,6 +39,133 @@ pub fn create_list_dir_tool() -> ToolSpec {
})
}
+pub fn create_grep_files_tool() -> ToolSpec {
+ let properties = BTreeMap::from([
+ (
+ "pattern".to_string(),
+ JsonSchema::string(Some(
+ "Regular expression pattern to search for.".to_string(),
+ )),
+ ),
+ (
+ "include".to_string(),
+ JsonSchema::string(Some(
+ "Optional glob that limits which files are searched (e.g. \"*.rs\" or \
+ \"*.{ts,tsx}\")."
+ .to_string(),
+ )),
+ ),
+ (
+ "path".to_string(),
+ JsonSchema::string(Some(
+ "Directory or file path to search. Defaults to the session's working directory."
+ .to_string(),
+ )),
+ ),
+ (
+ "limit".to_string(),
+ JsonSchema::number(Some(
+ "Maximum number of file paths to return (defaults to 100).".to_string(),
+ )),
+ ),
+ ]);
+
+ ToolSpec::Function(ResponsesApiTool {
+ name: "grep_files".to_string(),
+ description: "Finds files whose contents match the pattern and lists them by modification \
+ time."
+ .to_string(),
+ strict: false,
+ defer_loading: None,
+ parameters: JsonSchema::object(
+ properties,
+ Some(vec!["pattern".to_string()]),
+ Some(false.into()),
+ ),
+ output_schema: None,
+ })
+}
+
+pub fn create_read_file_tool() -> ToolSpec {
+ let indentation_properties = BTreeMap::from([
+ (
+ "anchor_line".to_string(),
+ JsonSchema::number(Some(
+ "Anchor line to center the indentation lookup on (defaults to offset).".to_string(),
+ )),
+ ),
+ (
+ "max_levels".to_string(),
+ JsonSchema::number(Some(
+ "How many parent indentation levels (smaller indents) to include.".to_string(),
+ )),
+ ),
+ (
+ "include_siblings".to_string(),
+ JsonSchema::boolean(Some(
+ "When true, include additional blocks that share the anchor indentation."
+ .to_string(),
+ )),
+ ),
+ (
+ "include_header".to_string(),
+ JsonSchema::boolean(Some(
+ "Include doc comments or attributes directly above the selected block.".to_string(),
+ )),
+ ),
+ (
+ "max_lines".to_string(),
+ JsonSchema::number(Some(
+ "Hard cap on the number of lines returned when using indentation mode.".to_string(),
+ )),
+ ),
+ ]);
+
+ let properties = BTreeMap::from([
+ (
+ "file_path".to_string(),
+ JsonSchema::string(Some("Absolute path to the file".to_string())),
+ ),
+ (
+ "offset".to_string(),
+ JsonSchema::number(Some(
+ "The line number to start reading from. Must be 1 or greater.".to_string(),
+ )),
+ ),
+ (
+ "limit".to_string(),
+ JsonSchema::number(Some("The maximum number of lines to return.".to_string())),
+ ),
+ (
+ "mode".to_string(),
+ JsonSchema::string(Some(
+ "Optional mode selector: \"slice\" for simple ranges (default) or \"indentation\" \
+ to expand around an anchor line."
+ .to_string(),
+ )),
+ ),
+ (
+ "indentation".to_string(),
+ JsonSchema::object(indentation_properties, None, Some(false.into())),
+ ),
+ ]);
+
+ ToolSpec::Function(ResponsesApiTool {
+ name: "read_file".to_string(),
+ description:
+ "Reads a local file with 1-indexed line numbers, supporting slice and indentation-aware block modes."
+ .to_string(),
+ strict: false,
+ defer_loading: None,
+ parameters: JsonSchema::object(
+ properties,
+ Some(vec!["file_path".to_string()]),
+ Some(false.into()),
+ ),
+ output_schema: None,
+ })
+}
+
pub fn create_test_sync_tool() -> ToolSpec {
let barrier_properties = BTreeMap::from([
(
diff --git a/codex-rs/tools/src/utility_tool_tests.rs b/codex-rs/tools/src/utility_tool_tests.rs
index 2984d02f4..429f6fb89 100644
--- a/codex-rs/tools/src/utility_tool_tests.rs
+++ b/codex-rs/tools/src/utility_tool_tests.rs
@@ -47,6 +47,147 @@ fn list_dir_tool_matches_expected_spec() {
);
}
+#[test]
+fn grep_files_tool_matches_expected_spec() {
+ assert_eq!(
+ create_grep_files_tool(),
+ ToolSpec::Function(ResponsesApiTool {
+ name: "grep_files".to_string(),
+ description: "Finds files whose contents match the pattern and lists them by modification \
+ time."
+ .to_string(),
+ strict: false,
+ defer_loading: None,
+ parameters: JsonSchema::object(
+ BTreeMap::from([
+ (
+ "include".to_string(),
+ JsonSchema::string(Some(
+ "Optional glob that limits which files are searched (e.g. \"*.rs\" or \
+ \"*.{ts,tsx}\")."
+ .to_string(),
+ )),
+ ),
+ (
+ "limit".to_string(),
+ JsonSchema::number(Some(
+ "Maximum number of file paths to return (defaults to 100)."
+ .to_string(),
+ )),
+ ),
+ (
+ "path".to_string(),
+ JsonSchema::string(Some(
+ "Directory or file path to search. Defaults to the session's working directory."
+ .to_string(),
+ )),
+ ),
+ (
+ "pattern".to_string(),
+ JsonSchema::string(Some(
+ "Regular expression pattern to search for.".to_string(),
+ )),
+ ),
+ ]),
+ Some(vec!["pattern".to_string()]),
+ Some(false.into()),
+ ),
+ output_schema: None,
+ })
+ );
+}
+
+#[test]
+fn read_file_tool_matches_expected_spec() {
+ assert_eq!(
+ create_read_file_tool(),
+ ToolSpec::Function(ResponsesApiTool {
+ name: "read_file".to_string(),
+ description:
+ "Reads a local file with 1-indexed line numbers, supporting slice and indentation-aware block modes."
+ .to_string(),
+ strict: false,
+ defer_loading: None,
+ parameters: JsonSchema::object(
+ BTreeMap::from([
+ (
+ "file_path".to_string(),
+ JsonSchema::string(Some("Absolute path to the file".to_string())),
+ ),
+ (
+ "indentation".to_string(),
+ JsonSchema::object(
+ BTreeMap::from([
+ (
+ "anchor_line".to_string(),
+ JsonSchema::number(Some(
+ "Anchor line to center the indentation lookup on (defaults to offset)."
+ .to_string(),
+ )),
+ ),
+ (
+ "include_header".to_string(),
+ JsonSchema::boolean(Some(
+ "Include doc comments or attributes directly above the selected block."
+ .to_string(),
+ )),
+ ),
+ (
+ "include_siblings".to_string(),
+ JsonSchema::boolean(Some(
+ "When true, include additional blocks that share the anchor indentation."
+ .to_string(),
+ )),
+ ),
+ (
+ "max_levels".to_string(),
+ JsonSchema::number(Some(
+ "How many parent indentation levels (smaller indents) to include."
+ .to_string(),
+ )),
+ ),
+ (
+ "max_lines".to_string(),
+ JsonSchema::number(Some(
+ "Hard cap on the number of lines returned when using indentation mode."
+ .to_string(),
+ )),
+ ),
+ ]),
+ None,
+ Some(false.into()),
+ ),
+ ),
+ (
+ "limit".to_string(),
+ JsonSchema::number(Some(
+ "The maximum number of lines to return.".to_string(),
+ )),
+ ),
+ (
+ "mode".to_string(),
+ JsonSchema::string(Some(
+ "Optional mode selector: \"slice\" for simple ranges (default) or \"indentation\" \
+ to expand around an anchor line."
+ .to_string(),
+ )),
+ ),
+ (
+ "offset".to_string(),
+ JsonSchema::number(Some(
+ "The line number to start reading from. Must be 1 or greater."
+ .to_string(),
+ )),
+ ),
+ ]),
+ Some(vec!["file_path".to_string()]),
+ Some(false.into()),
+ ),
+ output_schema: None,
+ })
+ );
+}
+
#[test]
fn test_sync_tool_matches_expected_spec() {
assert_eq!(
diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml
index 65f35b665..0e6d8bc15 100644
--- a/codex-rs/tui/Cargo.toml
+++ b/codex-rs/tui/Cargo.toml
@@ -31,6 +31,7 @@ codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-install-context = { workspace = true }
codex-chatgpt = { workspace = true }
+codex-clawbot = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-config = { workspace = true }
codex-connectors = { workspace = true }
@@ -55,6 +56,7 @@ codex-utils-cli = { workspace = true }
codex-utils-elapsed = { workspace = true }
codex-utils-fuzzy-match = { workspace = true }
codex-utils-oss = { workspace = true }
+codex-utils-rustls-provider = { workspace = true }
codex-utils-sandbox-summary = { workspace = true }
codex-utils-sleep-inhibitor = { workspace = true }
codex-utils-string = { workspace = true }
@@ -65,8 +67,11 @@ diffy = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] }
+humantime = "2"
+indexmap = { workspace = true, features = ["serde"] }
itertools = { workspace = true }
lazy_static = { workspace = true }
+notify = { workspace = true }
pathdiff = { workspace = true }
pulldown-cmark = { workspace = true }
rand = { workspace = true }
@@ -82,6 +87,7 @@ reqwest = { workspace = true, features = ["json"] }
rmcp = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
+serde_yaml = { workspace = true }
shlex = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index ab20b2214..7c7d77930 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -2,6 +2,7 @@ use crate::app_backtrack::BacktrackState;
use crate::app_command::AppCommand;
use crate::app_command::AppCommandView;
use crate::app_event::AppEvent;
+use crate::app_event::ClawbotControlsDestination;
use crate::app_event::ExitMode;
use crate::app_event::FeedbackCategory;
use crate::app_event::RateLimitRefreshOrigin;
@@ -26,6 +27,11 @@ use crate::chatwidget::ReplayKind;
use crate::chatwidget::ThreadInputState;
use crate::cwd_prompt::CwdPromptAction;
use crate::diff_render::DiffSummary;
+use crate::display_preferences::DisplayPreferences;
+use crate::display_preferences::display_preference_edit;
+use crate::display_preferences::set_display_preference_in_config;
+use crate::display_preferences_menu::DISPLAY_PREFERENCES_SELECTION_VIEW_ID;
+use crate::display_preferences_menu::display_preferences_panel_params;
use crate::exec_command::split_command_string;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::external_editor;
@@ -51,8 +57,6 @@ use crate::model_migration::migration_copy_for_models;
use crate::model_migration::run_model_migration_prompt;
use crate::multi_agents::agent_picker_status_dot_spans;
use crate::multi_agents::format_agent_picker_item_name;
-use crate::multi_agents::next_agent_shortcut_matches;
-use crate::multi_agents::previous_agent_shortcut_matches;
use crate::pager_overlay::Overlay;
use crate::read_session_model;
use crate::render::highlight::highlight_bash_to_lines;
@@ -102,6 +106,11 @@ use codex_app_server_protocol::ThreadStartSource;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnError as AppServerTurnError;
use codex_app_server_protocol::TurnStatus;
+use codex_clawbot::PendingClawbotTurn;
+#[cfg(test)]
+use codex_clawbot::ProviderOutboundReaction;
+#[cfg(test)]
+use codex_clawbot::ProviderOutboundTextMessage;
use codex_config::types::ApprovalsReviewer;
use codex_config::types::ModelAvailabilityNuxConfig;
use codex_exec_server::EnvironmentManager;
@@ -144,6 +153,7 @@ use ratatui::widgets::Paragraph;
use ratatui::widgets::Wrap;
use std::collections::BTreeMap;
use std::collections::HashMap;
+use std::collections::HashSet;
use std::collections::VecDeque;
use std::path::Path;
use std::path::PathBuf;
@@ -160,22 +170,57 @@ use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::mpsc::unbounded_channel;
use tokio::task::JoinHandle;
+#[cfg(test)]
+use tokio_util::sync::CancellationToken;
use toml::Value as TomlValue;
use uuid::Uuid;
mod agent_navigation;
mod app_server_adapter;
pub(crate) mod app_server_requests;
+mod btw;
+mod clawbot;
+mod clawbot_controller;
+mod clawbot_controls;
+mod editor_helpers;
+mod feature_dispatch;
+mod jump_navigation;
+mod key_chord;
mod loaded_threads;
mod pending_interactive_replay;
+mod popup_helpers;
+mod profile_controller;
+mod profile_management;
+mod thread_controller;
+mod thread_menu;
+mod workflow_controller;
+mod workflow_controls;
+mod workflow_definition;
+mod workflow_editor;
+mod workflow_file_watch;
+mod workflow_history;
+pub(crate) mod workflow_runtime;
+mod workflow_scheduler;
+mod workflow_yaml;
-use self::agent_navigation::AgentNavigationDirection;
use self::agent_navigation::AgentNavigationState;
use self::app_server_requests::PendingAppServerRequests;
+use self::btw::BtwSessionState;
+use self::key_chord::KeyChordAction;
+use self::key_chord::KeyChordResolution;
+use self::key_chord::KeyChordState;
use self::loaded_threads::find_loaded_subagent_threads_for_primary;
use self::pending_interactive_replay::PendingInteractiveReplayState;
+use self::workflow_file_watch::WorkflowFileWatchState;
+use self::workflow_history::WorkflowHistoryState;
+use self::workflow_scheduler::WorkflowSchedulerState;
const EXTERNAL_EDITOR_HINT: &str = "Save and close external editor to continue.";
const THREAD_EVENT_CHANNEL_CAPACITY: usize = 32768;
+#[derive(Clone, Copy)]
+enum ThreadLivenessRefreshMode {
+ Picker,
+ Selection,
+}
enum ThreadInteractiveRequest {
Approval(ApprovalRequest),
@@ -248,6 +293,36 @@ fn collab_receiver_thread_ids(notification: &ServerNotification) -> Option<&[Str
}
}
+fn workflow_after_turn_last_agent_message(
+ primary_thread_id: Option,
+ thread_id: ThreadId,
+ notification: &ServerNotification,
+) -> Option> {
+ if primary_thread_id != Some(thread_id) {
+ return None;
+ }
+ let ServerNotification::TurnCompleted(notification) = notification else {
+ return None;
+ };
+ if !matches!(
+ notification.turn.status,
+ TurnStatus::Completed | TurnStatus::Failed
+ ) {
+ return None;
+ }
+ Some(last_agent_message_for_turn(¬ification.turn))
+}
+
+fn last_agent_message_for_turn(turn: &Turn) -> Option