diff --git a/.github/actions/macos-code-sign/action.yml b/.github/actions/macos-code-sign/action.yml index ea4a19a8f5..200b23901f 100644 --- a/.github/actions/macos-code-sign/action.yml +++ b/.github/actions/macos-code-sign/action.yml @@ -132,9 +132,11 @@ runs: keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}") fi + entitlements_path="$GITHUB_ACTION_PATH/codex.entitlements.plist" + for binary in codex codex-responses-api-proxy; do path="codex-rs/target/${TARGET}/release/${binary}" - codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path" + codesign --force --options runtime --timestamp --entitlements "$entitlements_path" --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path" done - name: Notarize macOS binaries diff --git a/.github/actions/macos-code-sign/codex.entitlements.plist b/.github/actions/macos-code-sign/codex.entitlements.plist new file mode 100644 index 0000000000..d35e43ae58 --- /dev/null +++ b/.github/actions/macos-code-sign/codex.entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index e6eb1098fd..9db72b1d21 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -447,6 +447,24 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} + name: Configure musl rusty_v8 artifact overrides + env: + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)" + release_tag="rusty-v8-v${version}" + base_url="https://github.com/openai/codex/releases/download/${release_tag}" + archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz" + binding_dir="${RUNNER_TEMP}/rusty_v8" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + mkdir -p "${binding_dir}" + curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}" + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + - name: Install cargo-chef if: ${{ matrix.profile == 'release' }} uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2 diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 35078cf33d..4a3b35bd1c 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -210,6 +210,24 @@ jobs: echo "CFLAGS=${cflags}" >> "$GITHUB_ENV" echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV" + - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }} + name: Configure musl rusty_v8 artifact overrides + env: + TARGET: ${{ matrix.target }} + shell: bash + run: | + set -euo pipefail + version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)" + release_tag="rusty-v8-v${version}" + base_url="https://github.com/openai/codex/releases/download/${release_tag}" + archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz" + binding_dir="${RUNNER_TEMP}/rusty_v8" + binding_path="${binding_dir}/src_binding_release_${TARGET}.rs" + mkdir -p "${binding_dir}" + curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}" + echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV" + echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV" + - name: Cargo build shell: bash run: | diff --git a/MODULE.bazel b/MODULE.bazel index 812cad33bb..8564db701a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -144,6 +144,33 @@ crate.annotation( ) http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") +new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository") + +new_local_repository( + name = "v8_targets", + build_file = "//third_party/v8:BUILD.bazel", + path = "third_party/v8", +) + +crate.annotation( + build_script_data = [ + "@v8_targets//:rusty_v8_archive_for_target", + "@v8_targets//:rusty_v8_binding_for_target", + ], + build_script_env = { + "RUSTY_V8_ARCHIVE": "$(execpath @v8_targets//:rusty_v8_archive_for_target)", + "RUSTY_V8_SRC_BINDING_PATH": "$(execpath @v8_targets//:rusty_v8_binding_for_target)", + }, + crate = "v8", + gen_build_script = "on", + patch_args = ["-p1"], + patches = [ + "//patches:rusty_v8_prebuilt_out_dir.patch", + ], +) + +inject_repo(crate, "v8_targets") llvm = use_extension("@llvm//extensions:llvm.bzl", "llvm") use_repo(llvm, "llvm-project") @@ -210,6 +237,86 @@ http_archive( urls = ["https://static.crates.io/crates/v8/v8-146.4.0.crate"], ) +http_file( + name = "rusty_v8_146_4_0_aarch64_apple_darwin_archive", + downloaded_file_path = "librusty_v8_release_aarch64-apple-darwin.a.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-apple-darwin.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive", + downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-gnu.a.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_aarch64-unknown-linux-gnu.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive", + downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_x86_64_apple_darwin_archive", + downloaded_file_path = "librusty_v8_release_x86_64-apple-darwin.a.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-apple-darwin.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive", + downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-gnu.a.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive", + downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz", + urls = [ + "https://github.com/denoland/rusty_v8/releases/download/v146.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive", + downloaded_file_path = "librusty_v8_release_aarch64-unknown-linux-musl.a.gz", + urls = [ + "https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_aarch64-unknown-linux-musl.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding", + downloaded_file_path = "src_binding_release_aarch64-unknown-linux-musl.rs", + urls = [ + "https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_aarch64-unknown-linux-musl.rs", + ], +) + +http_file( + name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive", + downloaded_file_path = "librusty_v8_release_x86_64-unknown-linux-musl.a.gz", + urls = [ + "https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/librusty_v8_release_x86_64-unknown-linux-musl.a.gz", + ], +) + +http_file( + name = "rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding", + downloaded_file_path = "src_binding_release_x86_64-unknown-linux-musl.rs", + urls = [ + "https://github.com/openai/codex/releases/download/rusty-v8-v146.4.0/src_binding_release_x86_64-unknown-linux-musl.rs", + ], +) + use_repo(crate, "crates") bazel_dep(name = "libcap", version = "2.27.bcr.1") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 673bf6dfe9..6305cd169b 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -645,8 +645,8 @@ "atoi_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.14\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"num-traits/std\"]}}", "atomic-waker_1.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7.0\"}],\"features\":{}}", "autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}", - "aws-lc-rs_1.15.4": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.37.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", - "aws-lc-sys_0.37.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", + "aws-lc-rs_1.16.2": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.39.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", + "aws-lc-sys_0.39.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", "axum-core_0.5.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", @@ -680,6 +680,7 @@ "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\":{}}", "cached_proc_macro_types_0.1.1": "{\"dependencies\":[],\"features\":{}}", + "calendrical_calculations_0.2.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"core_maths\",\"req\":\"^0.1.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"}],\"features\":{\"logging\":[\"dep:log\"]}}", "cassowary_0.3.0": "{\"dependencies\":[],\"features\":{}}", "castaway_0.2.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "cbc_0.1.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"aes\",\"req\":\"^0.8\"},{\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"alloc\":[\"cipher/alloc\"],\"block-padding\":[\"cipher/block-padding\"],\"default\":[\"block-padding\"],\"std\":[\"cipher/std\",\"alloc\"],\"zeroize\":[\"cipher/zeroize\"]}}", @@ -723,6 +724,7 @@ "core-foundation-sys_0.8.7": "{\"dependencies\":[],\"features\":{\"default\":[\"link\"],\"link\":[],\"mac_os_10_7_support\":[],\"mac_os_10_8_features\":[]}}", "core-foundation_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-uuid\":[\"dep:uuid\"]}}", "core-foundation_0.9.4": "{\"dependencies\":[{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"default\":[\"link\"],\"link\":[\"core-foundation-sys/link\"],\"mac_os_10_7_support\":[\"core-foundation-sys/mac_os_10_7_support\"],\"mac_os_10_8_features\":[\"core-foundation-sys/mac_os_10_8_features\"],\"with-chrono\":[\"chrono\"],\"with-uuid\":[\"uuid\"]}}", + "core_maths_0.1.1": "{\"dependencies\":[{\"name\":\"libm\",\"req\":\"^0.2\"}],\"features\":{}}", "coreaudio-rs_0.11.3": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.0\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.3\"},{\"default_features\":false,\"name\":\"coreaudio-sys\",\"req\":\"^0.2\"}],\"features\":{\"audio_toolbox\":[\"coreaudio-sys/audio_toolbox\"],\"audio_unit\":[\"coreaudio-sys/audio_unit\"],\"core_audio\":[\"coreaudio-sys/core_audio\"],\"core_midi\":[\"coreaudio-sys/core_midi\"],\"default\":[\"audio_toolbox\",\"audio_unit\",\"core_audio\",\"open_al\",\"core_midi\"],\"open_al\":[\"coreaudio-sys/open_al\"]}}", "coreaudio-sys_0.2.17": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72\"}],\"features\":{\"audio_server_plugin\":[],\"audio_toolbox\":[],\"audio_unit\":[],\"core_audio\":[],\"core_midi\":[],\"default\":[\"audio_toolbox\",\"audio_unit\",\"core_audio\",\"audio_server_plugin\",\"open_al\",\"core_midi\"],\"io_kit_audio\":[],\"open_al\":[]}}", "cpal_0.15.3": "{\"dependencies\":[{\"name\":\"alsa\",\"req\":\"^0.9\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"asio-sys\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\",\"audio_toolbox\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"dasp_sample\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hound\",\"req\":\"^3.5\"},{\"name\":\"jack\",\"optional\":true,\"req\":\"^0.11\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"mach2\",\"req\":\"^0.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"name\":\"ndk\",\"req\":\"^0.8\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\"^0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.6\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"java-interface\"],\"name\":\"oboe\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ringbuf\",\"req\":\"^0.3\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.58\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.33\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"Win32_Media_Audio\",\"Win32_Foundation\",\"Win32_Devices_Properties\",\"Win32_Media_KernelStreaming\",\"Win32_System_Com_StructuredStorage\",\"Win32_System_Threading\",\"Win32_Security\",\"Win32_System_SystemServices\",\"Win32_System_Variant\",\"Win32_Media_Multimedia\",\"Win32_UI_Shell_PropertiesSystem\"],\"name\":\"windows\",\"req\":\"^0.54.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"asio\":[\"asio-sys\",\"num-traits\"],\"oboe-shared-stdcxx\":[\"oboe/shared-stdcxx\"]}}", @@ -778,6 +780,9 @@ "difflib_0.4.0": "{\"dependencies\":[],\"features\":{}}", "diffy_0.4.2": "{\"dependencies\":[{\"name\":\"nu-ansi-term\",\"req\":\"^0.50\"}],\"features\":{}}", "digest_0.10.7": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"block-buffer\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.4\"}],\"features\":{\"alloc\":[],\"core-api\":[\"block-buffer\"],\"default\":[\"core-api\"],\"dev\":[\"blobby\"],\"mac\":[\"subtle\"],\"oid\":[\"const-oid\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\"]}}", + "diplomat-runtime_0.14.0": "{\"dependencies\":[{\"name\":\"jni\",\"optional\":true,\"req\":\"^0.21\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"}],\"features\":{\"jvm-callback-support\":[\"dep:jni\"],\"log\":[\"dep:log\"]}}", + "diplomat_0.14.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"diplomat_core\",\"req\":\"^0.14.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.30\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2.0\"}],\"features\":{}}", + "diplomat_core_0.14.0": "{\"dependencies\":[{\"name\":\"displaydoc\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"either\",\"optional\":true,\"req\":\"^1.9.0\"},{\"features\":[\"yaml\"],\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"smallvec\",\"req\":\"^1.9.0\"},{\"features\":[\"ident\"],\"name\":\"strck\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"hir\":[\"either\"]}}", "dirs-next_2.0.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"dirs-sys-next\",\"req\":\"^0.1\"}],\"features\":{}}", "dirs-sys-next_0.1.2": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"name\":\"redox_users\",\"req\":\"^0.4.0\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"features\":[\"knownfolders\",\"objbase\",\"shlobj\",\"winbase\",\"winerror\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "dirs-sys_0.5.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"option-ext\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"redox_users\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"features\":[\"Win32_UI_Shell\",\"Win32_Foundation\",\"Win32_Globalization\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\">=0.59.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -846,6 +851,7 @@ "form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}", "fs_extra_1.3.0": "{\"dependencies\":[],\"features\":{}}", "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", + "fslock_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.66\",\"target\":\"cfg(unix)\"},{\"features\":[\"minwindef\",\"minwinbase\",\"winbase\",\"errhandlingapi\",\"winerror\",\"winnt\",\"synchapi\",\"handleapi\",\"fileapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-executor_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"name\":\"num_cpus\",\"optional\":true,\"req\":\"^1.8.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\",\"num_cpus\"]}}", @@ -875,6 +881,7 @@ "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\":\"\"}", "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\":[]}}", + "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\":[]}}", "hashbrown_0.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"bumpalo\",\"optional\":true,\"req\":\"^3.5.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"ahash-compile-time-rng\":[\"ahash/compile-time-rng\"],\"default\":[\"ahash\",\"inline-more\"],\"inline-more\":[],\"nightly\":[],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", @@ -912,6 +919,8 @@ "i18n-embed_0.15.4": "{\"dependencies\":[{\"name\":\"arc-swap\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"fluent\",\"optional\":true,\"req\":\"^0.16\"},{\"name\":\"fluent-langneg\",\"req\":\"^0.13\"},{\"name\":\"fluent-syntax\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"gettext\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"i18n-embed-impl\",\"optional\":true,\"req\":\"^0.8.4\"},{\"name\":\"intl-memoizer\",\"req\":\"^0.5\"},{\"name\":\"locale_config\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"maplit\",\"req\":\"^1.0\"},{\"name\":\"notify\",\"optional\":true,\"req\":\"^8.0.0\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"name\":\"rust-embed\",\"optional\":true,\"req\":\"^8.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tr\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"name\":\"walkdir\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"Window\",\"Navigator\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\"}],\"features\":{\"autoreload\":[\"notify\"],\"default\":[\"rust-embed\"],\"desktop-requester\":[\"locale_config\"],\"filesystem-assets\":[\"walkdir\"],\"fluent-system\":[\"fluent\",\"fluent-syntax\",\"parking_lot\",\"i18n-embed-impl\",\"i18n-embed-impl/fluent-system\",\"arc-swap\"],\"gettext-system\":[\"tr\",\"tr/gettext\",\"dep:gettext\",\"parking_lot\",\"i18n-embed-impl\",\"i18n-embed-impl/gettext-system\"],\"web-sys-requester\":[\"web-sys\"]}}", "iana-time-zone-haiku_0.1.2": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.79\"}],\"features\":{}}", "iana-time-zone_0.1.65": "{\"dependencies\":[{\"name\":\"android_system_properties\",\"req\":\"^0.1.5\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.1\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.6\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.1\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"iana-time-zone-haiku\",\"req\":\"^0.1.1\",\"target\":\"cfg(target_os = \\\"haiku\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.66\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"log\",\"req\":\"^0.4.14\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.46\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"windows-core\",\"req\":\">=0.56, <=0.62\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"fallback\":[]}}", + "icu_calendar_2.1.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"calendrical_calculations\",\"req\":\"^0.2.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"name\":\"icu_calendar_data\",\"optional\":true,\"req\":\"~2.1.1\"},{\"default_features\":false,\"name\":\"icu_locale\",\"optional\":true,\"req\":\"~2.1.1\"},{\"default_features\":false,\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"icu_provider\",\"req\":\"^2.1.1\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"name\":\"ixdtf\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"kind\":\"dev\",\"name\":\"simple_logger\",\"req\":\"^5.0.0\"},{\"default_features\":false,\"features\":[\"zerovec\"],\"name\":\"tinystr\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"ureq\",\"req\":\"^3.0.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"icu_locale_core/alloc\",\"tinystr/alloc\",\"serde?/alloc\"],\"compiled_data\":[\"dep:icu_calendar_data\",\"dep:icu_locale\",\"icu_locale?/compiled_data\",\"icu_provider/baked\"],\"datagen\":[\"serde\",\"dep:databake\",\"zerovec/databake\",\"tinystr/databake\",\"alloc\",\"icu_provider/export\"],\"default\":[\"compiled_data\",\"ixdtf\"],\"ixdtf\":[\"dep:ixdtf\"],\"logging\":[\"calendrical_calculations/logging\"],\"serde\":[\"dep:serde\",\"zerovec/serde\",\"tinystr/serde\",\"icu_provider/serde\"],\"unstable\":[]}}", + "icu_calendar_data_2.1.1": "{\"dependencies\":[],\"features\":{}}", "icu_collections_2.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"kind\":\"dev\",\"name\":\"iai\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"zerovec\"],\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"features\":[\"parse\"],\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"yoke\",\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"derive\",\"yoke\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde?/alloc\",\"zerovec/alloc\"],\"databake\":[\"dep:databake\",\"zerovec/databake\"],\"serde\":[\"dep:serde\",\"zerovec/serde\",\"potential_utf/serde\",\"alloc\"]}}", "icu_decimal_2.1.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"fixed_decimal\",\"req\":\"^0.7.0\"},{\"features\":[\"wasm_js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"icu_decimal_data\",\"optional\":true,\"req\":\"~2.1.1\"},{\"default_features\":false,\"name\":\"icu_locale\",\"optional\":true,\"req\":\"~2.1.1\"},{\"default_features\":false,\"name\":\"icu_locale_core\",\"req\":\"^2.1.1\"},{\"default_features\":false,\"name\":\"icu_provider\",\"req\":\"^2.1.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_distr\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"name\":\"writeable\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"zerovec\",\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde?/alloc\",\"zerovec/alloc\"],\"compiled_data\":[\"dep:icu_decimal_data\",\"dep:icu_locale\",\"icu_locale?/compiled_data\",\"icu_provider/baked\"],\"datagen\":[\"serde\",\"dep:databake\",\"zerovec/databake\",\"icu_provider/export\",\"alloc\"],\"default\":[\"compiled_data\"],\"ryu\":[\"fixed_decimal/ryu\"],\"serde\":[\"dep:serde\",\"icu_provider/serde\",\"zerovec/serde\"]}}", "icu_decimal_data_2.1.1": "{\"dependencies\":[],\"features\":{}}", @@ -955,6 +964,7 @@ "itertools_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itertools_0.14.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itoa_1.0.17": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{}}", + "ixdtf_0.6.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"serde-json-core\",\"req\":\"^0.6.0\"}],\"features\":{\"default\":[\"duration\"],\"duration\":[]}}", "jiff-static_0.2.18": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.93\"},{\"name\":\"quote\",\"req\":\"^1.0.38\"},{\"name\":\"syn\",\"req\":\"^2.0.98\"}],\"features\":{\"default\":[],\"perf-inline\":[],\"tz-fat\":[],\"tzdb\":[\"dep:jiff-tzdb\"]}}", "jiff_0.2.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.81\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.38\"},{\"kind\":\"dev\",\"name\":\"chrono-tz\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"hifitime\",\"req\":\"^3.9.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"humantime\",\"req\":\"^2.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.39.0\"},{\"name\":\"jiff-static\",\"req\":\"=0.2.18\",\"target\":\"cfg(any())\"},{\"name\":\"jiff-static\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.5\"},{\"name\":\"jiff-tzdb-platform\",\"optional\":true,\"req\":\"^0.1.3\",\"target\":\"cfg(any(windows, target_family = \\\"wasm\\\"))\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.50\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.21\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.21\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"req\":\"^1.10.0\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"name\":\"portable-atomic-util\",\"req\":\"^0.2.4\",\"target\":\"cfg(not(target_has_atomic = \\\"ptr\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.117\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.9.34\"},{\"kind\":\"dev\",\"name\":\"tabwriter\",\"req\":\"^1.4.0\"},{\"features\":[\"local-offset\",\"macros\",\"parsing\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.36\"},{\"kind\":\"dev\",\"name\":\"tzfile\",\"req\":\"^0.1.3\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.70\",\"target\":\"cfg(all(any(target_arch = \\\"wasm32\\\", target_arch = \\\"wasm64\\\"), target_os = \\\"unknown\\\"))\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_System_Time\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\">=0.52.0, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"portable-atomic-util/alloc\"],\"default\":[\"std\",\"tz-system\",\"tz-fat\",\"tzdb-bundle-platform\",\"tzdb-zoneinfo\",\"tzdb-concatenated\",\"perf-inline\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"logging\":[\"dep:log\"],\"perf-inline\":[],\"serde\":[\"dep:serde_core\"],\"static\":[\"static-tz\",\"jiff-static?/tzdb\"],\"static-tz\":[\"dep:jiff-static\"],\"std\":[\"alloc\",\"log?/std\",\"serde_core?/std\"],\"tz-fat\":[\"jiff-static?/tz-fat\"],\"tz-system\":[\"std\",\"dep:windows-sys\"],\"tzdb-bundle-always\":[\"dep:jiff-tzdb\",\"alloc\"],\"tzdb-bundle-platform\":[\"dep:jiff-tzdb-platform\",\"alloc\"],\"tzdb-concatenated\":[\"std\"],\"tzdb-zoneinfo\":[\"std\"]}}", "jni-sys_0.3.0": "{\"dependencies\":[],\"features\":{}}", @@ -1121,6 +1131,7 @@ "predicates-tree_1.0.12": "{\"dependencies\":[{\"features\":[\"color\"],\"kind\":\"dev\",\"name\":\"predicates\",\"req\":\"^3.1\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"name\":\"termtree\",\"req\":\"^0.5.0\"}],\"features\":{}}", "predicates_3.1.3": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"name\":\"difflib\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"float-cmp\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"normalize-line-endings\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"predicates-tree\",\"req\":\"^1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"color\":[],\"default\":[\"diff\",\"regex\",\"float-cmp\",\"normalize-line-endings\",\"color\"],\"diff\":[\"dep:difflib\"],\"unstable\":[]}}", "pretty_assertions_1.4.1": "{\"dependencies\":[{\"name\":\"diff\",\"req\":\"^0.1.12\"},{\"name\":\"yansi\",\"req\":\"^1.0.1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", + "prettyplease_0.2.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"default_features\":false,\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.105\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"extra-traits\",\"parsing\",\"printing\",\"visit-mut\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.105\"}],\"features\":{\"verbatim\":[\"syn/parsing\"]}}", "proc-macro-crate_3.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.94\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.39\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.99\"},{\"default_features\":false,\"features\":[\"parse\"],\"name\":\"toml_edit\",\"req\":\"^0.23.2\"}],\"features\":{}}", "proc-macro-error-attr2_2.0.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}", "proc-macro-error2_2.0.1": "{\"dependencies\":[{\"name\":\"proc-macro-error-attr2\",\"req\":\"=2.0.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.99\"}],\"features\":{\"default\":[\"syn-error\"],\"nightly\":[],\"syn-error\":[\"dep:syn\"]}}", @@ -1182,6 +1193,7 @@ "regex-syntax_0.8.8": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\",\"unicode\"],\"std\":[],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\"],\"unicode-age\":[],\"unicode-bool\":[],\"unicode-case\":[],\"unicode-gencat\":[],\"unicode-perl\":[],\"unicode-script\":[],\"unicode-segment\":[]}}", "regex_1.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aho-corasick\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.69\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"atty\",\"humantime\",\"termcolor\"],\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.3\"},{\"default_features\":false,\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.6.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"features\":[\"alloc\",\"syntax\",\"meta\",\"nfa-pikevm\"],\"name\":\"regex-automata\",\"req\":\"^0.4.12\"},{\"default_features\":false,\"name\":\"regex-syntax\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"regex-test\",\"req\":\"^0.1.0\"}],\"features\":{\"default\":[\"std\",\"perf\",\"unicode\",\"regex-syntax/default\"],\"logging\":[\"aho-corasick?/logging\",\"memchr?/logging\",\"regex-automata/logging\"],\"pattern\":[],\"perf\":[\"perf-cache\",\"perf-dfa\",\"perf-onepass\",\"perf-backtrack\",\"perf-inline\",\"perf-literal\"],\"perf-backtrack\":[\"regex-automata/nfa-backtrack\"],\"perf-cache\":[],\"perf-dfa\":[\"regex-automata/hybrid\"],\"perf-dfa-full\":[\"regex-automata/dfa-build\",\"regex-automata/dfa-search\"],\"perf-inline\":[\"regex-automata/perf-inline\"],\"perf-literal\":[\"dep:aho-corasick\",\"dep:memchr\",\"regex-automata/perf-literal\"],\"perf-onepass\":[\"regex-automata/dfa-onepass\"],\"std\":[\"aho-corasick?/std\",\"memchr?/std\",\"regex-automata/std\",\"regex-syntax/std\"],\"unicode\":[\"unicode-age\",\"unicode-bool\",\"unicode-case\",\"unicode-gencat\",\"unicode-perl\",\"unicode-script\",\"unicode-segment\",\"regex-automata/unicode\",\"regex-syntax/unicode\"],\"unicode-age\":[\"regex-automata/unicode-age\",\"regex-syntax/unicode-age\"],\"unicode-bool\":[\"regex-automata/unicode-bool\",\"regex-syntax/unicode-bool\"],\"unicode-case\":[\"regex-automata/unicode-case\",\"regex-syntax/unicode-case\"],\"unicode-gencat\":[\"regex-automata/unicode-gencat\",\"regex-syntax/unicode-gencat\"],\"unicode-perl\":[\"regex-automata/unicode-perl\",\"regex-automata/unicode-word-boundary\",\"regex-syntax/unicode-perl\"],\"unicode-script\":[\"regex-automata/unicode-script\",\"regex-syntax/unicode-script\"],\"unicode-segment\":[\"regex-automata/unicode-segment\",\"regex-syntax/unicode-segment\"],\"unstable\":[\"pattern\"],\"use_std\":[\"std\"]}}", "reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}", + "resb_0.1.1": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"nom\",\"optional\":true,\"req\":\"^7.0.0\"},{\"default_features\":false,\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.220\"}],\"features\":{\"default\":[],\"logging\":[\"dep:log\"],\"serialize\":[\"std\"],\"std\":[],\"text\":[\"dep:indexmap\",\"dep:nom\",\"std\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", @@ -1200,7 +1212,7 @@ "rustix_1.1.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.177\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.11.0\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", - "rustls-webpki_0.103.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", + "rustls-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\"]}}", @@ -1293,6 +1305,7 @@ "starlark_syntax_0.13.0": "{\"dependencies\":[{\"name\":\"allocative\",\"req\":\"^0.3.4\"},{\"name\":\"annotate-snippets\",\"req\":\"^0.9.0\"},{\"name\":\"anyhow\",\"req\":\"^1.0.65\"},{\"name\":\"derivative\",\"req\":\"^2.2\"},{\"features\":[\"full\"],\"name\":\"derive_more\",\"req\":\"^1.0.0\"},{\"name\":\"dupe\",\"req\":\"^0.9.0\"},{\"kind\":\"build\",\"name\":\"lalrpop\",\"req\":\"^0.19.7\"},{\"name\":\"lalrpop-util\",\"req\":\"^0.19.7\"},{\"name\":\"logos\",\"req\":\"^0.12\"},{\"name\":\"lsp-types\",\"req\":\"^0.94.1\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"num-bigint\",\"req\":\"^0.4.3\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"starlark_map\",\"req\":\"^0.13.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0.36\"}],\"features\":{}}", "static_assertions_1.1.0": "{\"dependencies\":[],\"features\":{\"nightly\":[]}}", "stop-words_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"human_regex\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"constructed\":[],\"default\":[\"iso\"],\"iso\":[],\"nltk\":[],\"unimplemented\":[]}}", + "strck_1.0.0": "{\"dependencies\":[{\"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\":\"smol_str\",\"req\":\"^0.3\"},{\"name\":\"unicode-ident\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"ident\":[\"dep:unicode-ident\"]}}", "streaming-iterator_0.1.9": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "string_cache_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"serde_support\"],\"serde_support\":[\"serde\"]}}", "stringprep_0.1.5": "{\"dependencies\":[{\"name\":\"unicode-bidi\",\"req\":\"^0.3\"},{\"name\":\"unicode-normalization\",\"req\":\"^0.1\"},{\"name\":\"unicode-properties\",\"req\":\"^0.1.1\"}],\"features\":{}}", @@ -1317,6 +1330,8 @@ "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\"]}}", "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\":[]}}", + "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\"]}}", + "temporal_rs_0.1.2": "{\"dependencies\":[{\"name\":\"core_maths\",\"req\":\"^0.1.1\"},{\"name\":\"iana-time-zone\",\"optional\":true,\"req\":\"^0.1.64\"},{\"default_features\":false,\"features\":[\"unstable\",\"compiled_data\"],\"name\":\"icu_calendar\",\"req\":\"^2.1.0\"},{\"name\":\"icu_locale\",\"req\":\"^2.1.0\"},{\"features\":[\"duration\"],\"name\":\"ixdtf\",\"req\":\"^0.6.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.28\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.19\"},{\"name\":\"timezone_provider\",\"req\":\"^0.1.2\"},{\"name\":\"tinystr\",\"req\":\"^0.8.0\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1.0\"},{\"name\":\"writeable\",\"req\":\"^0.6.0\"}],\"features\":{\"compiled_data\":[\"tzdb\"],\"default\":[\"sys\"],\"float64_representable_durations\":[],\"log\":[\"dep:log\"],\"std\":[],\"sys\":[\"std\",\"compiled_data\",\"dep:web-time\",\"dep:iana-time-zone\"],\"tzdb\":[\"std\",\"timezone_provider/tzif\"]}}", "term_0.7.0": "{\"dependencies\":[{\"name\":\"dirs-next\",\"req\":\"^2\"},{\"name\":\"rustversion\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"features\":[\"consoleapi\",\"wincon\",\"handleapi\",\"fileapi\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[]}}", "termcolor_1.4.1": "{\"dependencies\":[{\"name\":\"winapi-util\",\"req\":\"^0.1.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "terminal_size_0.4.3": "{\"dependencies\":[{\"features\":[\"termios\"],\"name\":\"rustix\",\"req\":\"^1.0.1\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\"],\"name\":\"windows-sys\",\"req\":\"^0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -1337,6 +1352,7 @@ "time-core_0.1.8": "{\"dependencies\":[],\"features\":{\"large-dates\":[]}}", "time-macros_0.2.27": "{\"dependencies\":[{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"}],\"features\":{\"formatting\":[],\"large-dates\":[],\"parsing\":[],\"serde\":[]}}", "time_0.3.47": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8.1\",\"target\":\"cfg(bench)\"},{\"features\":[\"powerfmt\"],\"name\":\"deranged\",\"req\":\"^0.5.2\"},{\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"num_threads\",\"optional\":true,\"req\":\"^0.1.2\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"default_features\":false,\"name\":\"powerfmt\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26.1\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.184\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.68\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.126\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"},{\"name\":\"time-macros\",\"optional\":true,\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"time-macros\",\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.102\",\"target\":\"cfg(__ui_tests)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\"],\"default\":[\"std\"],\"formatting\":[\"dep:itoa\",\"std\",\"time-macros?/formatting\"],\"large-dates\":[\"time-core/large-dates\",\"time-macros?/large-dates\"],\"local-offset\":[\"std\",\"dep:libc\",\"dep:num_threads\"],\"macros\":[\"dep:time-macros\"],\"parsing\":[\"time-macros?/parsing\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\",\"deranged/quickcheck\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\",\"deranged/rand08\"],\"rand09\":[\"dep:rand09\",\"deranged/rand09\"],\"serde\":[\"dep:serde_core\",\"time-macros?/serde\",\"deranged/serde\"],\"serde-human-readable\":[\"serde\",\"formatting\",\"parsing\"],\"serde-well-known\":[\"serde\",\"formatting\",\"parsing\"],\"std\":[\"alloc\"],\"wasm-bindgen\":[\"dep:js-sys\"]}}", + "timezone_provider_0.1.2": "{\"dependencies\":[{\"name\":\"combine\",\"optional\":true,\"req\":\"^4.6.7\"},{\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.4\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.225\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.145\"},{\"features\":[\"zerovec\"],\"name\":\"tinystr\",\"req\":\"^0.8.0\"},{\"name\":\"tzif\",\"optional\":true,\"req\":\"^0.4.0\"},{\"features\":[\"derive\"],\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"zerotrie\",\"req\":\"^0.2.0\"},{\"features\":[\"derive\",\"alloc\"],\"name\":\"zerovec\",\"req\":\"^0.11.0\"},{\"name\":\"zoneinfo64\",\"optional\":true,\"req\":\"^0.2.0\"},{\"features\":[\"std\"],\"name\":\"zoneinfo_rs\",\"optional\":true,\"req\":\"^0.0.18\"}],\"features\":{\"datagen\":[\"std\",\"dep:serde\",\"dep:databake\",\"dep:yoke\",\"dep:serde_json\",\"tinystr/serde\",\"tinystr/databake\",\"zerotrie/serde\",\"zerotrie/databake\",\"zerovec/serde\",\"zerovec/databake\",\"zerovec/derive\",\"dep:zoneinfo_rs\",\"experimental_tzif\"],\"default\":[],\"experimental_tzif\":[],\"std\":[],\"tzif\":[\"dep:tzif\",\"dep:jiff-tzdb\",\"dep:combine\",\"std\"],\"zoneinfo64\":[\"dep:zoneinfo64\"]}}", "tiny-keccak_2.0.2": "{\"dependencies\":[{\"name\":\"crunchy\",\"req\":\"^0.2.2\"}],\"features\":{\"cshake\":[],\"default\":[],\"fips202\":[\"keccak\",\"shake\",\"sha3\"],\"k12\":[],\"keccak\":[],\"kmac\":[\"cshake\"],\"parallel_hash\":[\"cshake\"],\"sha3\":[],\"shake\":[],\"sp800\":[\"cshake\",\"kmac\",\"tuple_hash\"],\"tuple_hash\":[\"cshake\"]}}", "tiny_http_0.12.0": "{\"dependencies\":[{\"name\":\"ascii\",\"req\":\"^1.0\"},{\"name\":\"chunked_transfer\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"fdlimit\",\"req\":\"^0.1\"},{\"name\":\"httpdate\",\"req\":\"^1.0.2\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rustc-serialize\",\"req\":\"^0.3\"},{\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.20\"},{\"name\":\"rustls-pemfile\",\"optional\":true,\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.6.0\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[],\"ssl\":[\"ssl-openssl\"],\"ssl-openssl\":[\"openssl\",\"zeroize\"],\"ssl-rustls\":[\"rustls\",\"rustls-pemfile\",\"zeroize\"]}}", "tinystr_0.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\"],\"std\":[],\"zerovec\":[\"dep:zerovec\"]}}", @@ -1412,6 +1428,7 @@ "utf8_iter_1.0.4": "{\"dependencies\":[],\"features\":{}}", "utf8parse_0.2.2": "{\"dependencies\":[],\"features\":{\"default\":[],\"nightly\":[]}}", "uuid_1.20.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"atomic\",\"optional\":true,\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh-derive\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.20.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"default_features\":false,\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"), target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"name\":\"md-5\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\"))))\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.56\"},{\"default_features\":false,\"name\":\"sha1_smol\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"slog\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.52\"},{\"name\":\"uuid-rng-internal-lib\",\"optional\":true,\"package\":\"uuid-rng-internal\",\"req\":\"^1.20.0\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"default_features\":false,\"features\":[\"msrv\"],\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", any(target_os = \\\"unknown\\\", target_os = \\\"none\\\")))\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"}],\"features\":{\"atomic\":[\"dep:atomic\"],\"borsh\":[\"dep:borsh\",\"dep:borsh-derive\"],\"default\":[\"std\"],\"fast-rng\":[\"rng\",\"dep:rand\"],\"js\":[\"dep:wasm-bindgen\",\"dep:js-sys\"],\"macro-diagnostics\":[],\"md5\":[\"dep:md-5\"],\"rng\":[\"dep:getrandom\"],\"rng-getrandom\":[\"rng\",\"dep:getrandom\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/getrandom\"],\"rng-rand\":[\"rng\",\"dep:rand\",\"uuid-rng-internal-lib\",\"uuid-rng-internal-lib/rand\"],\"serde\":[\"dep:serde_core\"],\"sha1\":[\"dep:sha1_smol\"],\"std\":[\"wasm-bindgen?/std\",\"js-sys?/std\"],\"v1\":[\"atomic\"],\"v3\":[\"md5\"],\"v4\":[\"rng\"],\"v5\":[\"sha1\"],\"v6\":[\"atomic\"],\"v7\":[\"rng\"],\"v8\":[]}}", + "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_check_0.9.5": "{\"dependencies\":[],\"features\":{}}", @@ -1442,6 +1459,7 @@ "webpki-roots_0.26.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"parent\",\"package\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "webpki-roots_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "weezl_0.1.12": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.12\"},{\"default_features\":false,\"features\":[\"macros\",\"io-util\",\"net\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"compat\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.2\"}],\"features\":{\"alloc\":[],\"async\":[\"futures\",\"std\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "which_6.0.3": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.9.0\"},{\"name\":\"home\",\"req\":\"^0.5.9\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^0.38.30\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "which_8.0.0": "{\"dependencies\":[{\"name\":\"env_home\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"optional\":true,\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"optional\":true,\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"real-sys\"],\"real-sys\":[\"dep:env_home\",\"dep:rustix\",\"dep:winsafe\"],\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "whoami_1.6.1": "{\"dependencies\":[{\"name\":\"libredox\",\"req\":\"^0.1.1\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"wasite\",\"req\":\"^0.1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"web\"],\"web\":[\"web-sys\"]}}", "widestring_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"features\":[\"Win32_System_Diagnostics_Debug\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.59\"}],\"features\":{\"alloc\":[],\"debugger_visualizer\":[\"alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", @@ -1548,6 +1566,7 @@ "zerovec_0.11.5": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\"},{\"default_features\":false,\"features\":[\"xxhash64\"],\"name\":\"twox-hash\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"yoke\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"name\":\"zerofrom\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"zerovec-derive\",\"optional\":true,\"req\":\"^0.11.1\"}],\"features\":{\"alloc\":[\"serde?/alloc\"],\"databake\":[\"dep:databake\"],\"derive\":[\"dep:zerovec-derive\"],\"hashmap\":[\"dep:twox-hash\",\"alloc\"],\"serde\":[\"dep:serde\"],\"std\":[],\"yoke\":[\"dep:yoke\"]}}", "zip_2.4.2": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.95\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"req\":\"^1.4.1\",\"target\":\"cfg(fuzzing)\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bzip2\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"=4.4.18\"},{\"name\":\"constant_time_eq\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crc32fast\",\"req\":\"^1.4\"},{\"name\":\"crossbeam-utils\",\"req\":\"^0.8.21\",\"target\":\"cfg(any(all(target_arch = \\\"arm\\\", target_pointer_width = \\\"32\\\"), target_arch = \\\"mips\\\", target_arch = \\\"powerpc\\\"))\"},{\"name\":\"deflate64\",\"optional\":true,\"req\":\"^0.1.9\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"wasm_js\",\"std\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.1\"},{\"features\":[\"wasm_js\",\"std\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.3.1\"},{\"features\":[\"reset\"],\"name\":\"hmac\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"lzma-rs\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"memchr\",\"req\":\"^2.7\"},{\"default_features\":false,\"name\":\"nt-time\",\"optional\":true,\"req\":\"^0.10.6\"},{\"name\":\"pbkdf2\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.15\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.37\"},{\"default_features\":false,\"features\":[\"formatting\",\"macros\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.37\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5\"},{\"name\":\"xz2\",\"optional\":true,\"req\":\"^0.1.7\"},{\"features\":[\"zeroize_derive\"],\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"},{\"name\":\"zopfli\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13\"}],\"features\":{\"_all-features\":[],\"_deflate-any\":[],\"aes-crypto\":[\"aes\",\"constant_time_eq\",\"hmac\",\"pbkdf2\",\"sha1\",\"getrandom\",\"zeroize\"],\"chrono\":[\"chrono/default\"],\"default\":[\"aes-crypto\",\"bzip2\",\"deflate64\",\"deflate\",\"lzma\",\"time\",\"zstd\",\"xz\"],\"deflate\":[\"flate2/rust_backend\",\"deflate-zopfli\",\"deflate-flate2\"],\"deflate-flate2\":[\"_deflate-any\"],\"deflate-miniz\":[\"deflate\",\"deflate-flate2\"],\"deflate-zlib\":[\"flate2/zlib\",\"deflate-flate2\"],\"deflate-zlib-ng\":[\"flate2/zlib-ng\",\"deflate-flate2\"],\"deflate-zopfli\":[\"zopfli\",\"_deflate-any\"],\"lzma\":[\"lzma-rs/stream\"],\"nt-time\":[\"dep:nt-time\"],\"unreserved\":[],\"xz\":[\"dep:xz2\"]}}", "zmij_1.0.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\":{}}", + "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_7.2.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zstd-sys\",\"req\":\"^2.0.15\"}],\"features\":{\"arrays\":[],\"bindgen\":[\"zstd-sys/bindgen\"],\"debug\":[\"zstd-sys/debug\"],\"default\":[\"legacy\",\"arrays\",\"zdict_builder\"],\"doc-cfg\":[],\"experimental\":[\"zstd-sys/experimental\"],\"fat-lto\":[\"zstd-sys/fat-lto\"],\"legacy\":[\"zstd-sys/legacy\"],\"no_asm\":[\"zstd-sys/no_asm\"],\"pkg-config\":[\"zstd-sys/pkg-config\"],\"seekable\":[\"zstd-sys/seekable\"],\"std\":[\"zstd-sys/std\"],\"thin\":[\"zstd-sys/thin\"],\"thin-lto\":[\"zstd-sys/thin-lto\"],\"zdict_builder\":[\"zstd-sys/zdict_builder\"],\"zstdmt\":[\"zstd-sys/zstdmt\"]}}", "zstd-sys_2.0.16+zstd.1.5.7": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.45\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.28\"}],\"features\":{\"debug\":[],\"default\":[\"legacy\",\"zdict_builder\",\"bindgen\"],\"experimental\":[],\"fat-lto\":[],\"legacy\":[],\"no_asm\":[],\"no_wasm_shim\":[],\"non-cargo\":[],\"pkg-config\":[],\"seekable\":[],\"std\":[],\"thin\":[],\"thin-lto\":[],\"zdict_builder\":[],\"zstdmt\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 6725310631..d0917ee38d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -800,9 +800,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -811,9 +811,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -949,6 +949,8 @@ dependencies = [ "cexpr", "clang-sys", "itertools 0.13.0", + "log", + "prettyplease", "proc-macro2", "quote", "regex", @@ -1152,6 +1154,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -1584,7 +1596,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "url", - "which", + "which 8.0.0", "wiremock", "zip", ] @@ -1788,6 +1800,20 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codex-code-mode" +version = "0.0.0" +dependencies = [ + "async-trait", + "pretty_assertions", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "v8", +] + [[package]] name = "codex-config" version = "0.0.0" @@ -1845,6 +1871,7 @@ dependencies = [ "codex-arg0", "codex-artifacts", "codex-async-utils", + "codex-code-mode", "codex-config", "codex-connectors", "codex-exec-server", @@ -1930,7 +1957,7 @@ dependencies = [ "url", "uuid", "walkdir", - "which", + "which 8.0.0", "wildmatch", "windows-sys 0.52.0", "wiremock", @@ -2179,7 +2206,7 @@ dependencies = [ "serde_json", "tokio", "tracing", - "which", + "which 8.0.0", "wiremock", ] @@ -2432,7 +2459,7 @@ dependencies = [ "tracing", "urlencoding", "webbrowser", - "which", + "which 8.0.0", ] [[package]] @@ -2472,7 +2499,7 @@ dependencies = [ "tree-sitter", "tree-sitter-bash", "url", - "which", + "which 8.0.0", ] [[package]] @@ -2644,7 +2671,7 @@ dependencies = [ "uuid", "vt100", "webbrowser", - "which", + "which 8.0.0", "windows-sys 0.52.0", "winsplit", ] @@ -2736,7 +2763,7 @@ dependencies = [ "uuid", "vt100", "webbrowser", - "which", + "which 8.0.0", "windows-sys 0.52.0", "winsplit", ] @@ -2907,6 +2934,14 @@ dependencies = [ "regex-lite", ] +[[package]] +name = "codex-v8-poc" +version = "0.0.0" +dependencies = [ + "pretty_assertions", + "v8", +] + [[package]] name = "codex-windows-sandbox" version = "0.0.0" @@ -3112,6 +3147,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "core_test_support" version = "0.0.0" @@ -3717,6 +3761,38 @@ dependencies = [ "subtle", ] +[[package]] +name = "diplomat" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "diplomat-runtime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29" + +[[package]] +name = "diplomat_core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck", + "syn 2.0.114", +] + [[package]] name = "dirs" version = "6.0.0" @@ -4323,6 +4399,16 @@ dependencies = [ "libc", ] +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.31" @@ -4551,6 +4637,15 @@ dependencies = [ "regex-syntax 0.8.8", ] +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + [[package]] name = "h2" version = "0.4.13" @@ -5008,6 +5103,28 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + [[package]] name = "icu_collections" version = "2.1.1" @@ -5442,6 +5559,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + [[package]] name = "jiff" version = "0.2.18" @@ -7167,6 +7290,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -8000,6 +8133,16 @@ dependencies = [ "webpki-roots 1.0.5", ] +[[package]] +name = "resb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76" +dependencies = [ + "potential_utf", + "serde_core", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -8240,9 +8383,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -9380,6 +9523,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "strck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f" +dependencies = [ + "unicode-ident", +] + [[package]] name = "streaming-iterator" version = "0.1.9" @@ -9624,6 +9776,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "temporal_capi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8" +dependencies = [ + "diplomat", + "diplomat-runtime", + "icu_calendar", + "icu_locale", + "num-traits", + "temporal_rs", + "timezone_provider", + "writeable", + "zoneinfo64", +] + +[[package]] +name = "temporal_rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1" +dependencies = [ + "core_maths", + "icu_calendar", + "icu_locale", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "writeable", +] + [[package]] name = "term" version = "0.7.0" @@ -9831,6 +10016,18 @@ dependencies = [ "time-core", ] +[[package]] +name = "timezone_provider" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993" +dependencies = [ + "tinystr", + "zerotrie", + "zerovec", + "zoneinfo64", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -10630,6 +10827,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v8" +version = "146.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1" +dependencies = [ + "bindgen", + "bitflags 2.10.0", + "fslock", + "gzip-header", + "home", + "miniz_oxide", + "paste", + "temporal_capi", + "which 6.0.3", +] + [[package]] name = "valuable" version = "0.1.1" @@ -10929,6 +11143,18 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "which" version = "8.0.0" @@ -11932,6 +12158,19 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +[[package]] +name = "zoneinfo64" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0" +dependencies = [ + "calendrical_calculations", + "icu_locale_core", + "potential_utf", + "resb", + "serde", +] + [[package]] name = "zopfli" version = "0.8.3" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 331174a802..524b61e3b8 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -13,6 +13,7 @@ members = [ "feedback", "features", "codex-backend-openapi-models", + "code-mode", "cloud-requirements", "cloud-tasks", "cloud-tasks-client", @@ -45,6 +46,7 @@ members = [ "otel", "tui", "tui_app_server", + "v8-poc", "utils/absolute-path", "utils/cargo-bin", "utils/git", @@ -90,6 +92,7 @@ app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } codex-api = { path = "codex-api" } codex-artifacts = { path = "artifacts" } +codex-code-mode = { path = "code-mode" } codex-package-manager = { path = "package-manager" } codex-app-server = { path = "app-server" } codex-app-server-client = { path = "app-server-client" } @@ -137,6 +140,7 @@ codex-test-macros = { path = "test-macros" } codex-terminal-detection = { path = "terminal-detection" } codex-tui = { path = "tui" } codex-tui-app-server = { path = "tui_app_server" } +codex-v8-poc = { path = "v8-poc" } codex-utils-absolute-path = { path = "utils/absolute-path" } codex-utils-approval-presets = { path = "utils/approval-presets" } codex-utils-cache = { path = "utils/cache" } @@ -245,6 +249,7 @@ regex-lite = "0.1.8" reqwest = "0.12" rmcp = { version = "0.15.0", default-features = false } runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" } +v8 = "=146.4.0" rustls = { version = "0.23", default-features = false, features = [ "ring", "std", @@ -370,7 +375,8 @@ ignored = [ "icu_provider", "openssl-sys", "codex-utils-readiness", - "codex-secrets" + "codex-secrets", + "codex-v8-poc", ] [profile.release] diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 423d90085c..5b06ab539c 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3068,6 +3068,26 @@ ], "type": "object" }, + "ThreadRealtimeTranscriptUpdatedNotification": { + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "role", + "text", + "threadId" + ], + "type": "object" + }, "ThreadStartedNotification": { "properties": { "thread": { @@ -4565,6 +4585,26 @@ "title": "Thread/realtime/itemAddedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/transcriptUpdated" + ], + "title": "Thread/realtime/transcriptUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/transcriptUpdatedNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 261411ded2..c24c8ac249 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -4274,6 +4274,26 @@ "title": "Thread/realtime/itemAddedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/transcriptUpdated" + ], + "title": "Thread/realtime/transcriptUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadRealtimeTranscriptUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/transcriptUpdatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -13042,6 +13062,28 @@ "title": "ThreadRealtimeStartedNotification", "type": "object" }, + "ThreadRealtimeTranscriptUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "role", + "text", + "threadId" + ], + "title": "ThreadRealtimeTranscriptUpdatedNotification", + "type": "object" + }, "ThreadResumeParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index e48397d3f3..c479da94e4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -8709,6 +8709,26 @@ "title": "Thread/realtime/itemAddedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/transcriptUpdated" + ], + "title": "Thread/realtime/transcriptUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/transcriptUpdatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -10802,6 +10822,28 @@ "title": "ThreadRealtimeStartedNotification", "type": "object" }, + "ThreadRealtimeTranscriptUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "role", + "text", + "threadId" + ], + "title": "ThreadRealtimeTranscriptUpdatedNotification", + "type": "object" + }, "ThreadResumeParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeTranscriptUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeTranscriptUpdatedNotification.json new file mode 100644 index 0000000000..2c6860fa31 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRealtimeTranscriptUpdatedNotification.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "role", + "text", + "threadId" + ], + "title": "ThreadRealtimeTranscriptUpdatedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index f8796deb79..d9e2df7797 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -41,6 +41,7 @@ import type { ThreadRealtimeErrorNotification } from "./v2/ThreadRealtimeErrorNo import type { ThreadRealtimeItemAddedNotification } from "./v2/ThreadRealtimeItemAddedNotification"; import type { ThreadRealtimeOutputAudioDeltaNotification } from "./v2/ThreadRealtimeOutputAudioDeltaNotification"; import type { ThreadRealtimeStartedNotification } from "./v2/ThreadRealtimeStartedNotification"; +import type { ThreadRealtimeTranscriptUpdatedNotification } from "./v2/ThreadRealtimeTranscriptUpdatedNotification"; import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; @@ -55,4 +56,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcriptUpdated", "params": ThreadRealtimeTranscriptUpdatedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeTranscriptUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeTranscriptUpdatedNotification.ts new file mode 100644 index 0000000000..d2940029f2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadRealtimeTranscriptUpdatedNotification.ts @@ -0,0 +1,9 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - flat transcript delta emitted whenever realtime + * transcript text changes. + */ +export type ThreadRealtimeTranscriptUpdatedNotification = { threadId: string, role: string, text: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index f98d7676ff..d9cc4758bc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -281,6 +281,7 @@ export type { ThreadRealtimeErrorNotification } from "./ThreadRealtimeErrorNotif export type { ThreadRealtimeItemAddedNotification } from "./ThreadRealtimeItemAddedNotification"; export type { ThreadRealtimeOutputAudioDeltaNotification } from "./ThreadRealtimeOutputAudioDeltaNotification"; export type { ThreadRealtimeStartedNotification } from "./ThreadRealtimeStartedNotification"; +export type { ThreadRealtimeTranscriptUpdatedNotification } from "./ThreadRealtimeTranscriptUpdatedNotification"; export type { ThreadResumeParams } from "./ThreadResumeParams"; export type { ThreadResumeResponse } from "./ThreadResumeResponse"; export type { ThreadRollbackParams } from "./ThreadRollbackParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7e1dc78f20..56897566d9 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -923,6 +923,8 @@ server_notification_definitions! { ThreadRealtimeStarted => "thread/realtime/started" (v2::ThreadRealtimeStartedNotification), #[experimental("thread/realtime/itemAdded")] ThreadRealtimeItemAdded => "thread/realtime/itemAdded" (v2::ThreadRealtimeItemAddedNotification), + #[experimental("thread/realtime/transcriptUpdated")] + ThreadRealtimeTranscriptUpdated => "thread/realtime/transcriptUpdated" (v2::ThreadRealtimeTranscriptUpdatedNotification), #[experimental("thread/realtime/outputAudio/delta")] ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification), #[experimental("thread/realtime/error")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index d43581aaf7..57017833a6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3787,6 +3787,17 @@ pub struct ThreadRealtimeItemAddedNotification { pub item: JsonValue, } +/// EXPERIMENTAL - flat transcript delta emitted whenever realtime +/// transcript text changes. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeTranscriptUpdatedNotification { + pub thread_id: String, + pub role: String, + pub text: String, +} + /// EXPERIMENTAL - streamed output audio emitted by thread realtime. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5ada340492..3248a44424 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -825,7 +825,8 @@ The fuzzy file search session API emits per-query notifications: The thread realtime API emits thread-scoped notifications for session lifecycle and streaming media: - `thread/realtime/started` — `{ threadId, sessionId }` once realtime starts for the thread (experimental). -- `thread/realtime/itemAdded` — `{ threadId, item }` for non-audio realtime items (experimental). `item` is forwarded as raw JSON while the upstream websocket item schema remains unstable. +- `thread/realtime/itemAdded` — `{ threadId, item }` for raw non-audio realtime items that do not have a dedicated typed app-server notification, including `handoff_request` (experimental). `item` is forwarded as raw JSON while the upstream websocket item schema remains unstable. +- `thread/realtime/transcriptUpdated` — `{ threadId, role, text }` whenever realtime transcript text changes (experimental). This forwards the live transcript delta from that realtime event, not the full accumulated transcript. - `thread/realtime/outputAudio/delta` — `{ threadId, audio }` for streamed output audio chunks (experimental). `audio` uses camelCase fields (`data`, `sampleRate`, `numChannels`, `samplesPerChannel`). - `thread/realtime/error` — `{ threadId, message }` when realtime encounters a transport or backend error (experimental). - `thread/realtime/closed` — `{ threadId, reason }` when the realtime transport closes (experimental). diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 6b69393474..34640a50cf 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -86,6 +86,7 @@ use codex_app_server_protocol::ThreadRealtimeErrorNotification; use codex_app_server_protocol::ThreadRealtimeItemAddedNotification; use codex_app_server_protocol::ThreadRealtimeOutputAudioDeltaNotification; use codex_app_server_protocol::ThreadRealtimeStartedNotification; +use codex_app_server_protocol::ThreadRealtimeTranscriptUpdatedNotification; use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification; @@ -397,8 +398,30 @@ pub(crate) async fn apply_bespoke_event_handling( )) .await; } - RealtimeEvent::InputTranscriptDelta(_) => {} - RealtimeEvent::OutputTranscriptDelta(_) => {} + RealtimeEvent::InputTranscriptDelta(event) => { + let notification = ThreadRealtimeTranscriptUpdatedNotification { + thread_id: conversation_id.to_string(), + role: "user".to_string(), + text: event.delta, + }; + outgoing + .send_server_notification( + ServerNotification::ThreadRealtimeTranscriptUpdated(notification), + ) + .await; + } + RealtimeEvent::OutputTranscriptDelta(event) => { + let notification = ThreadRealtimeTranscriptUpdatedNotification { + thread_id: conversation_id.to_string(), + role: "assistant".to_string(), + text: event.delta, + }; + outgoing + .send_server_notification( + ServerNotification::ThreadRealtimeTranscriptUpdated(notification), + ) + .await; + } RealtimeEvent::AudioOut(audio) => { let notification = ThreadRealtimeOutputAudioDeltaNotification { thread_id: conversation_id.to_string(), diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index a95871430a..89b1090b84 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -25,6 +25,7 @@ use wiremock::ResponseTemplate; use wiremock::matchers::header; use wiremock::matchers::method; use wiremock::matchers::path; +use wiremock::matchers::query_param; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -678,6 +679,7 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu .await; Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) .respond_with( @@ -784,6 +786,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { .await; Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) @@ -850,6 +853,7 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) .mount(&server) .await; @@ -888,6 +892,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) .expect(1) .mount(&server) diff --git a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs index 1073c1b938..e8a39efa64 100644 --- a/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs +++ b/codex-rs/app-server/tests/suite/v2/realtime_conversation.rs @@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadRealtimeStartResponse; use codex_app_server_protocol::ThreadRealtimeStartedNotification; use codex_app_server_protocol::ThreadRealtimeStopParams; use codex_app_server_protocol::ThreadRealtimeStopResponse; +use codex_app_server_protocol::ThreadRealtimeTranscriptUpdatedNotification; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_features::FEATURES; @@ -66,6 +67,24 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { "content": [{ "type": "text", "text": "hi" }] } }), + json!({ + "type": "conversation.item.input_audio_transcription.delta", + "delta": "delegate now" + }), + json!({ + "type": "response.output_text.delta", + "delta": "working" + }), + json!({ + "type": "conversation.item.done", + "item": { + "id": "item_2", + "type": "function_call", + "name": "codex", + "call_id": "handoff_1", + "arguments": "{\"input_transcript\":\"delegate now\"}" + } + }), json!({ "type": "error", "message": "upstream boom" @@ -180,6 +199,40 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> { assert_eq!(item_added.thread_id, output_audio.thread_id); assert_eq!(item_added.item["type"], json!("message")); + let first_transcript_update = read_notification::( + &mut mcp, + "thread/realtime/transcriptUpdated", + ) + .await?; + assert_eq!(first_transcript_update.thread_id, output_audio.thread_id); + assert_eq!(first_transcript_update.role, "user"); + assert_eq!(first_transcript_update.text, "delegate now"); + + let second_transcript_update = + read_notification::( + &mut mcp, + "thread/realtime/transcriptUpdated", + ) + .await?; + assert_eq!(second_transcript_update.thread_id, output_audio.thread_id); + assert_eq!(second_transcript_update.role, "assistant"); + assert_eq!(second_transcript_update.text, "working"); + + let handoff_item_added = read_notification::( + &mut mcp, + "thread/realtime/itemAdded", + ) + .await?; + assert_eq!(handoff_item_added.thread_id, output_audio.thread_id); + assert_eq!(handoff_item_added.item["type"], json!("handoff_request")); + assert_eq!(handoff_item_added.item["handoff_id"], json!("handoff_1")); + assert_eq!(handoff_item_added.item["item_id"], json!("item_2")); + assert_eq!( + handoff_item_added.item["input_transcript"], + json!("delegate now") + ); + assert_eq!(handoff_item_added.item["active_transcript"], json!([])); + let realtime_error = read_notification::(&mut mcp, "thread/realtime/error") .await?; diff --git a/codex-rs/code-mode/BUILD.bazel b/codex-rs/code-mode/BUILD.bazel new file mode 100644 index 0000000000..bf39d9d5a5 --- /dev/null +++ b/codex-rs/code-mode/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "code-mode", + crate_name = "codex_code_mode", +) diff --git a/codex-rs/code-mode/Cargo.toml b/codex-rs/code-mode/Cargo.toml new file mode 100644 index 0000000000..e821ca0e4d --- /dev/null +++ b/codex-rs/code-mode/Cargo.toml @@ -0,0 +1,25 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-code-mode" +version.workspace = true + +[lib] +doctest = false +name = "codex_code_mode" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +async-trait = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } +tokio-util = { workspace = true, features = ["rt"] } +tracing = { workspace = true } +v8 = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs new file mode 100644 index 0000000000..c875e2a1b1 --- /dev/null +++ b/codex-rs/code-mode/src/description.rs @@ -0,0 +1,555 @@ +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; + +use crate::PUBLIC_TOOL_NAME; + +const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1; +const CODE_MODE_ONLY_PREFACE: &str = + "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"; +const EXEC_DESCRIPTION_TEMPLATE: &str = r#"## exec +- Runs raw JavaScript in an isolated context (no Node, no file system, or network access, no console). +- Send raw JavaScript source text, not JSON, quoted strings, or markdown code fences. +- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. +- `yield_time_ms` asks `exec` to yield early after that many milliseconds if the script is still running. +- `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. +- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`. +- Tool methods take either string or object as parameter. +- They return either a structured value or a string based on the description above. + +- Global helpers: +- `exit()`: Immediately ends the current script successfully (like an early return from the top level). +- `text(value: string | number | boolean | undefined | null)`: Appends a text item. Non-string values are stringified with `JSON.stringify(...)` when possible. +- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null })`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. +- `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. +- `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. +- `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`. +- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries. +- `yield_control()`: yields the accumulated output to the model immediately while the script keeps running."#; +const WAIT_DESCRIPTION_TEMPLATE: &str = r#"- Use `wait` only after `exec` returns `Script running with cell ID ...`. +- `cell_id` identifies the running `exec` cell to resume. +- `yield_time_ms` controls how long to wait for more output before yielding again. If omitted, `wait` uses its default wait timeout. +- `max_tokens` limits how much new output this wait call returns. +- `terminate: true` stops the running cell instead of waiting for more output. +- `wait` returns only the new output since the last yield, or the final completion or termination result for that cell. +- If the cell is still running, `wait` may yield again with the same `cell_id`. +- If the cell has already finished, `wait` returns the completed result and closes the cell."#; + +pub const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:"; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CodeModeToolKind { + Function, + Freeform, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ToolDefinition { + pub name: String, + pub description: String, + pub kind: CodeModeToolKind, + pub input_schema: Option, + pub output_schema: Option, +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct CodeModeExecPragma { + #[serde(default)] + yield_time_ms: Option, + #[serde(default)] + max_output_tokens: Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParsedExecSource { + pub code: String, + pub yield_time_ms: Option, + pub max_output_tokens: Option, +} + +pub fn parse_exec_source(input: &str) -> Result { + if input.trim().is_empty() { + return Err( + "exec expects raw JavaScript source text (non-empty). Provide JS only, optionally with first-line `// @exec: {\"yield_time_ms\": 10000, \"max_output_tokens\": 1000}`.".to_string(), + ); + } + + let mut args = ParsedExecSource { + code: input.to_string(), + yield_time_ms: None, + max_output_tokens: None, + }; + + let mut lines = input.splitn(2, '\n'); + let first_line = lines.next().unwrap_or_default(); + let rest = lines.next().unwrap_or_default(); + let trimmed = first_line.trim_start(); + let Some(pragma) = trimmed.strip_prefix(CODE_MODE_PRAGMA_PREFIX) else { + return Ok(args); + }; + + if rest.trim().is_empty() { + return Err( + "exec pragma must be followed by JavaScript source on subsequent lines".to_string(), + ); + } + + let directive = pragma.trim(); + if directive.is_empty() { + return Err( + "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" + .to_string(), + ); + } + + let value: serde_json::Value = serde_json::from_str(directive).map_err(|err| { + format!( + "exec pragma must be valid JSON with supported fields `yield_time_ms` and `max_output_tokens`: {err}" + ) + })?; + let object = value.as_object().ok_or_else(|| { + "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" + .to_string() + })?; + for key in object.keys() { + match key.as_str() { + "yield_time_ms" | "max_output_tokens" => {} + _ => { + return Err(format!( + "exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `{key}`" + )); + } + } + } + + let pragma: CodeModeExecPragma = serde_json::from_value(value).map_err(|err| { + format!( + "exec pragma fields `yield_time_ms` and `max_output_tokens` must be non-negative safe integers: {err}" + ) + })?; + if pragma + .yield_time_ms + .is_some_and(|yield_time_ms| yield_time_ms > MAX_JS_SAFE_INTEGER) + { + return Err( + "exec pragma field `yield_time_ms` must be a non-negative safe integer".to_string(), + ); + } + if pragma.max_output_tokens.is_some_and(|max_output_tokens| { + u64::try_from(max_output_tokens) + .map(|max_output_tokens| max_output_tokens > MAX_JS_SAFE_INTEGER) + .unwrap_or(true) + }) { + return Err( + "exec pragma field `max_output_tokens` must be a non-negative safe integer".to_string(), + ); + } + + args.code = rest.to_string(); + args.yield_time_ms = pragma.yield_time_ms; + args.max_output_tokens = pragma.max_output_tokens; + Ok(args) +} + +pub fn is_code_mode_nested_tool(tool_name: &str) -> bool { + tool_name != crate::PUBLIC_TOOL_NAME && tool_name != crate::WAIT_TOOL_NAME +} + +pub fn build_exec_tool_description( + enabled_tools: &[(String, String)], + code_mode_only: bool, +) -> String { + if !code_mode_only { + return EXEC_DESCRIPTION_TEMPLATE.to_string(); + } + + let mut sections = vec![ + CODE_MODE_ONLY_PREFACE.to_string(), + EXEC_DESCRIPTION_TEMPLATE.to_string(), + ]; + + if !enabled_tools.is_empty() { + let nested_tool_reference = enabled_tools + .iter() + .map(|(name, nested_description)| { + let global_name = normalize_code_mode_identifier(name); + format!( + "### `{global_name}` (`{name}`)\n{}", + nested_description.trim() + ) + }) + .collect::>() + .join("\n\n"); + sections.push(nested_tool_reference); + } + + sections.join("\n\n") +} + +pub fn build_wait_tool_description() -> &'static str { + WAIT_DESCRIPTION_TEMPLATE +} + +pub fn normalize_code_mode_identifier(tool_key: &str) -> String { + let mut identifier = String::new(); + + for (index, ch) in tool_key.chars().enumerate() { + let is_valid = if index == 0 { + ch == '_' || ch == '$' || ch.is_ascii_alphabetic() + } else { + ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() + }; + + if is_valid { + identifier.push(ch); + } else { + identifier.push('_'); + } + } + + if identifier.is_empty() { + "_".to_string() + } else { + identifier + } +} + +pub fn augment_tool_definition(mut definition: ToolDefinition) -> ToolDefinition { + if definition.name != PUBLIC_TOOL_NAME { + definition.description = append_code_mode_sample_for_definition(&definition); + } + definition +} + +pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata { + EnabledToolMetadata { + tool_name: definition.name.clone(), + global_name: normalize_code_mode_identifier(&definition.name), + description: definition.description.clone(), + kind: definition.kind, + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct EnabledToolMetadata { + pub tool_name: String, + pub global_name: String, + pub description: String, + pub kind: CodeModeToolKind, +} + +pub fn append_code_mode_sample( + description: &str, + tool_name: &str, + input_name: &str, + input_type: String, + output_type: String, +) -> String { + let declaration = format!( + "declare const tools: {{ {} }};", + render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type) + ); + format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```") +} + +fn append_code_mode_sample_for_definition(definition: &ToolDefinition) -> String { + let input_name = match definition.kind { + CodeModeToolKind::Function => "args", + CodeModeToolKind::Freeform => "input", + }; + let input_type = match definition.kind { + CodeModeToolKind::Function => definition + .input_schema + .as_ref() + .map(render_json_schema_to_typescript) + .unwrap_or_else(|| "unknown".to_string()), + CodeModeToolKind::Freeform => "string".to_string(), + }; + let output_type = definition + .output_schema + .as_ref() + .map(render_json_schema_to_typescript) + .unwrap_or_else(|| "unknown".to_string()); + append_code_mode_sample( + &definition.description, + &definition.name, + input_name, + input_type, + output_type, + ) +} + +fn render_code_mode_tool_declaration( + tool_name: &str, + input_name: &str, + input_type: String, + output_type: String, +) -> String { + let tool_name = normalize_code_mode_identifier(tool_name); + format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;") +} + +pub fn render_json_schema_to_typescript(schema: &JsonValue) -> String { + render_json_schema_to_typescript_inner(schema) +} + +fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String { + match schema { + JsonValue::Bool(true) => "unknown".to_string(), + JsonValue::Bool(false) => "never".to_string(), + JsonValue::Object(map) => { + if let Some(value) = map.get("const") { + return render_json_schema_literal(value); + } + + if let Some(values) = map.get("enum").and_then(JsonValue::as_array) { + let rendered = values + .iter() + .map(render_json_schema_literal) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + + for key in ["anyOf", "oneOf"] { + if let Some(variants) = map.get(key).and_then(JsonValue::as_array) { + let rendered = variants + .iter() + .map(render_json_schema_to_typescript_inner) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + } + + if let Some(variants) = map.get("allOf").and_then(JsonValue::as_array) { + let rendered = variants + .iter() + .map(render_json_schema_to_typescript_inner) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" & "); + } + } + + if let Some(schema_type) = map.get("type") { + if let Some(types) = schema_type.as_array() { + let rendered = types + .iter() + .filter_map(JsonValue::as_str) + .map(|schema_type| render_json_schema_type_keyword(map, schema_type)) + .collect::>(); + if !rendered.is_empty() { + return rendered.join(" | "); + } + } + + if let Some(schema_type) = schema_type.as_str() { + return render_json_schema_type_keyword(map, schema_type); + } + } + + if map.contains_key("properties") + || map.contains_key("additionalProperties") + || map.contains_key("required") + { + return render_json_schema_object(map); + } + + if map.contains_key("items") || map.contains_key("prefixItems") { + return render_json_schema_array(map); + } + + "unknown".to_string() + } + _ => "unknown".to_string(), + } +} + +fn render_json_schema_type_keyword( + map: &serde_json::Map, + schema_type: &str, +) -> String { + match schema_type { + "string" => "string".to_string(), + "number" | "integer" => "number".to_string(), + "boolean" => "boolean".to_string(), + "null" => "null".to_string(), + "array" => render_json_schema_array(map), + "object" => render_json_schema_object(map), + _ => "unknown".to_string(), + } +} + +fn render_json_schema_array(map: &serde_json::Map) -> String { + if let Some(items) = map.get("items") { + let item_type = render_json_schema_to_typescript_inner(items); + return format!("Array<{item_type}>"); + } + + if let Some(items) = map.get("prefixItems").and_then(JsonValue::as_array) { + let item_types = items + .iter() + .map(render_json_schema_to_typescript_inner) + .collect::>(); + if !item_types.is_empty() { + return format!("[{}]", item_types.join(", ")); + } + } + + "unknown[]".to_string() +} + +fn render_json_schema_object(map: &serde_json::Map) -> String { + let required = map + .get("required") + .and_then(JsonValue::as_array) + .map(|items| { + items + .iter() + .filter_map(JsonValue::as_str) + .collect::>() + }) + .unwrap_or_default(); + let properties = map + .get("properties") + .and_then(JsonValue::as_object) + .cloned() + .unwrap_or_default(); + + let mut sorted_properties = properties.iter().collect::>(); + sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); + let mut lines = sorted_properties + .into_iter() + .map(|(name, value)| { + let optional = if required.iter().any(|required_name| required_name == name) { + "" + } else { + "?" + }; + let property_name = render_json_schema_property_name(name); + let property_type = render_json_schema_to_typescript_inner(value); + format!("{property_name}{optional}: {property_type};") + }) + .collect::>(); + + if let Some(additional_properties) = map.get("additionalProperties") { + let property_type = match additional_properties { + JsonValue::Bool(true) => Some("unknown".to_string()), + JsonValue::Bool(false) => None, + value => Some(render_json_schema_to_typescript_inner(value)), + }; + + if let Some(property_type) = property_type { + lines.push(format!("[key: string]: {property_type};")); + } + } else if properties.is_empty() { + lines.push("[key: string]: unknown;".to_string()); + } + + if lines.is_empty() { + return "{}".to_string(); + } + + format!("{{ {} }}", lines.join(" ")) +} + +fn render_json_schema_property_name(name: &str) -> String { + if normalize_code_mode_identifier(name) == name { + name.to_string() + } else { + serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\""))) + } +} + +fn render_json_schema_literal(value: &JsonValue) -> String { + serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string()) +} + +#[cfg(test)] +mod tests { + use super::CodeModeToolKind; + use super::ParsedExecSource; + use super::ToolDefinition; + use super::augment_tool_definition; + use super::build_exec_tool_description; + use super::normalize_code_mode_identifier; + use super::parse_exec_source; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn parse_exec_source_without_pragma() { + assert_eq!( + parse_exec_source("text('hi')").unwrap(), + ParsedExecSource { + code: "text('hi')".to_string(), + yield_time_ms: None, + max_output_tokens: None, + } + ); + } + + #[test] + fn parse_exec_source_with_pragma() { + assert_eq!( + parse_exec_source("// @exec: {\"yield_time_ms\": 10}\ntext('hi')").unwrap(), + ParsedExecSource { + code: "text('hi')".to_string(), + yield_time_ms: Some(10), + max_output_tokens: None, + } + ); + } + + #[test] + fn normalize_identifier_rewrites_invalid_characters() { + assert_eq!( + "mcp__ologs__get_profile", + normalize_code_mode_identifier("mcp__ologs__get_profile") + ); + assert_eq!( + "hidden_dynamic_tool", + normalize_code_mode_identifier("hidden-dynamic-tool") + ); + } + + #[test] + fn augment_tool_definition_appends_typed_declaration() { + let definition = ToolDefinition { + name: "hidden_dynamic_tool".to_string(), + description: "Test tool".to_string(), + kind: CodeModeToolKind::Function, + input_schema: Some(json!({ + "type": "object", + "properties": { "city": { "type": "string" } }, + "required": ["city"], + "additionalProperties": false + })), + output_schema: Some(json!({ + "type": "object", + "properties": { "ok": { "type": "boolean" } }, + "required": ["ok"] + })), + }; + + let description = augment_tool_definition(definition).description; + assert!(description.contains("declare const tools")); + assert!( + description.contains( + "hidden_dynamic_tool(args: { city: string; }): Promise<{ ok: boolean; }>;" + ) + ); + } + + #[test] + fn code_mode_only_description_includes_nested_tools() { + let description = + build_exec_tool_description(&[("foo".to_string(), "bar".to_string())], true); + assert!(description.contains("### `foo` (`foo`)")); + } +} diff --git a/codex-rs/code-mode/src/lib.rs b/codex-rs/code-mode/src/lib.rs new file mode 100644 index 0000000000..841e568be3 --- /dev/null +++ b/codex-rs/code-mode/src/lib.rs @@ -0,0 +1,30 @@ +mod description; +mod response; +mod runtime; +mod service; + +pub use description::CODE_MODE_PRAGMA_PREFIX; +pub use description::CodeModeToolKind; +pub use description::ToolDefinition; +pub use description::append_code_mode_sample; +pub use description::augment_tool_definition; +pub use description::build_exec_tool_description; +pub use description::build_wait_tool_description; +pub use description::is_code_mode_nested_tool; +pub use description::normalize_code_mode_identifier; +pub use description::parse_exec_source; +pub use description::render_json_schema_to_typescript; +pub use response::FunctionCallOutputContentItem; +pub use response::ImageDetail; +pub use runtime::DEFAULT_EXEC_YIELD_TIME_MS; +pub use runtime::DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL; +pub use runtime::DEFAULT_WAIT_YIELD_TIME_MS; +pub use runtime::ExecuteRequest; +pub use runtime::RuntimeResponse; +pub use runtime::WaitRequest; +pub use service::CodeModeService; +pub use service::CodeModeTurnHost; +pub use service::CodeModeTurnWorker; + +pub const PUBLIC_TOOL_NAME: &str = "exec"; +pub const WAIT_TOOL_NAME: &str = "wait"; diff --git a/codex-rs/code-mode/src/response.rs b/codex-rs/code-mode/src/response.rs new file mode 100644 index 0000000000..43579fac85 --- /dev/null +++ b/codex-rs/code-mode/src/response.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ImageDetail { + Auto, + Low, + High, + Original, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum FunctionCallOutputContentItem { + InputText { + text: String, + }, + InputImage { + image_url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + detail: Option, + }, +} diff --git a/codex-rs/code-mode/src/runtime/callbacks.rs b/codex-rs/code-mode/src/runtime/callbacks.rs new file mode 100644 index 0000000000..b77ae82d68 --- /dev/null +++ b/codex-rs/code-mode/src/runtime/callbacks.rs @@ -0,0 +1,209 @@ +use crate::response::FunctionCallOutputContentItem; + +use super::EXIT_SENTINEL; +use super::RuntimeEvent; +use super::RuntimeState; +use super::value::json_to_v8; +use super::value::normalize_output_image; +use super::value::serialize_output_text; +use super::value::throw_type_error; +use super::value::v8_value_to_json; + +pub(super) fn tool_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let tool_name = args.data().to_rust_string_lossy(scope); + let input = if args.length() == 0 { + Ok(None) + } else { + v8_value_to_json(scope, args.get(0)) + }; + let input = match input { + Ok(input) => input, + Err(error_text) => { + throw_type_error(scope, &error_text); + return; + } + }; + + let Some(resolver) = v8::PromiseResolver::new(scope) else { + throw_type_error(scope, "failed to create tool promise"); + return; + }; + let promise = resolver.get_promise(scope); + + let resolver = v8::Global::new(scope, resolver); + let Some(state) = scope.get_slot_mut::() else { + throw_type_error(scope, "runtime state unavailable"); + return; + }; + let id = format!("tool-{}", state.next_tool_call_id); + state.next_tool_call_id = state.next_tool_call_id.saturating_add(1); + let event_tx = state.event_tx.clone(); + state.pending_tool_calls.insert(id.clone(), resolver); + let _ = event_tx.send(RuntimeEvent::ToolCall { + id, + name: tool_name, + input, + }); + retval.set(promise.into()); +} + +pub(super) fn text_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let value = if args.length() == 0 { + v8::undefined(scope).into() + } else { + args.get(0) + }; + let text = match serialize_output_text(scope, value) { + Ok(text) => text, + Err(error_text) => { + throw_type_error(scope, &error_text); + return; + } + }; + if let Some(state) = scope.get_slot::() { + let _ = state.event_tx.send(RuntimeEvent::ContentItem( + FunctionCallOutputContentItem::InputText { text }, + )); + } + retval.set(v8::undefined(scope).into()); +} + +pub(super) fn image_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let value = if args.length() == 0 { + v8::undefined(scope).into() + } else { + args.get(0) + }; + let image_item = match normalize_output_image(scope, value) { + Ok(image_item) => image_item, + Err(()) => return, + }; + if let Some(state) = scope.get_slot::() { + let _ = state.event_tx.send(RuntimeEvent::ContentItem(image_item)); + } + retval.set(v8::undefined(scope).into()); +} + +pub(super) fn store_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + _retval: v8::ReturnValue, +) { + let key = match args.get(0).to_string(scope) { + Some(key) => key.to_rust_string_lossy(scope), + None => { + throw_type_error(scope, "store key must be a string"); + return; + } + }; + let value = args.get(1); + let serialized = match v8_value_to_json(scope, value) { + Ok(Some(value)) => value, + Ok(None) => { + throw_type_error( + scope, + &format!("Unable to store {key:?}. Only plain serializable objects can be stored."), + ); + return; + } + Err(error_text) => { + throw_type_error(scope, &error_text); + return; + } + }; + if let Some(state) = scope.get_slot_mut::() { + state.stored_values.insert(key, serialized); + } +} + +pub(super) fn load_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let key = match args.get(0).to_string(scope) { + Some(key) => key.to_rust_string_lossy(scope), + None => { + throw_type_error(scope, "load key must be a string"); + return; + } + }; + let value = scope + .get_slot::() + .and_then(|state| state.stored_values.get(&key)) + .cloned(); + let Some(value) = value else { + retval.set(v8::undefined(scope).into()); + return; + }; + let Some(value) = json_to_v8(scope, &value) else { + throw_type_error(scope, "failed to load stored value"); + return; + }; + retval.set(value); +} + +pub(super) fn notify_callback( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + mut retval: v8::ReturnValue, +) { + let value = if args.length() == 0 { + v8::undefined(scope).into() + } else { + args.get(0) + }; + let text = match serialize_output_text(scope, value) { + Ok(text) => text, + Err(error_text) => { + throw_type_error(scope, &error_text); + return; + } + }; + if text.trim().is_empty() { + throw_type_error(scope, "notify expects non-empty text"); + return; + } + if let Some(state) = scope.get_slot::() { + let _ = state.event_tx.send(RuntimeEvent::Notify { + call_id: state.tool_call_id.clone(), + text, + }); + } + retval.set(v8::undefined(scope).into()); +} + +pub(super) fn yield_control_callback( + scope: &mut v8::PinScope<'_, '_>, + _args: v8::FunctionCallbackArguments, + _retval: v8::ReturnValue, +) { + if let Some(state) = scope.get_slot::() { + let _ = state.event_tx.send(RuntimeEvent::YieldRequested); + } +} + +pub(super) fn exit_callback( + scope: &mut v8::PinScope<'_, '_>, + _args: v8::FunctionCallbackArguments, + _retval: v8::ReturnValue, +) { + if let Some(state) = scope.get_slot_mut::() { + state.exit_requested = true; + } + if let Some(error) = v8::String::new(scope, EXIT_SENTINEL) { + scope.throw_exception(error.into()); + } +} diff --git a/codex-rs/code-mode/src/runtime/globals.rs b/codex-rs/code-mode/src/runtime/globals.rs new file mode 100644 index 0000000000..371479497b --- /dev/null +++ b/codex-rs/code-mode/src/runtime/globals.rs @@ -0,0 +1,138 @@ +use super::RuntimeState; +use super::callbacks::exit_callback; +use super::callbacks::image_callback; +use super::callbacks::load_callback; +use super::callbacks::notify_callback; +use super::callbacks::store_callback; +use super::callbacks::text_callback; +use super::callbacks::tool_callback; +use super::callbacks::yield_control_callback; + +pub(super) fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), String> { + let global = scope.get_current_context().global(scope); + let console = v8::String::new(scope, "console") + .ok_or_else(|| "failed to allocate global `console`".to_string())?; + if global.delete(scope, console.into()) != Some(true) { + return Err("failed to remove global `console`".to_string()); + } + + let tools = build_tools_object(scope)?; + let all_tools = build_all_tools_value(scope)?; + let text = helper_function(scope, "text", text_callback)?; + let image = helper_function(scope, "image", image_callback)?; + let store = helper_function(scope, "store", store_callback)?; + let load = helper_function(scope, "load", load_callback)?; + let notify = helper_function(scope, "notify", notify_callback)?; + let yield_control = helper_function(scope, "yield_control", yield_control_callback)?; + let exit = helper_function(scope, "exit", exit_callback)?; + + set_global(scope, global, "tools", tools.into())?; + set_global(scope, global, "ALL_TOOLS", all_tools)?; + set_global(scope, global, "text", text.into())?; + set_global(scope, global, "image", image.into())?; + set_global(scope, global, "store", store.into())?; + set_global(scope, global, "load", load.into())?; + set_global(scope, global, "notify", notify.into())?; + set_global(scope, global, "yield_control", yield_control.into())?; + set_global(scope, global, "exit", exit.into())?; + Ok(()) +} + +fn build_tools_object<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> Result, String> { + let tools = v8::Object::new(scope); + let enabled_tools = scope + .get_slot::() + .map(|state| state.enabled_tools.clone()) + .unwrap_or_default(); + + for tool in enabled_tools { + let name = v8::String::new(scope, &tool.global_name) + .ok_or_else(|| "failed to allocate tool name".to_string())?; + let function = tool_function(scope, &tool.tool_name)?; + tools.set(scope, name.into(), function.into()); + } + Ok(tools) +} + +fn build_all_tools_value<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> Result, String> { + let enabled_tools = scope + .get_slot::() + .map(|state| state.enabled_tools.clone()) + .unwrap_or_default(); + let array = v8::Array::new(scope, enabled_tools.len() as i32); + let name_key = v8::String::new(scope, "name") + .ok_or_else(|| "failed to allocate ALL_TOOLS name key".to_string())?; + let description_key = v8::String::new(scope, "description") + .ok_or_else(|| "failed to allocate ALL_TOOLS description key".to_string())?; + + for (index, tool) in enabled_tools.iter().enumerate() { + let item = v8::Object::new(scope); + let name = v8::String::new(scope, &tool.global_name) + .ok_or_else(|| "failed to allocate ALL_TOOLS name".to_string())?; + let description = v8::String::new(scope, &tool.description) + .ok_or_else(|| "failed to allocate ALL_TOOLS description".to_string())?; + + if item.set(scope, name_key.into(), name.into()) != Some(true) { + return Err("failed to set ALL_TOOLS name".to_string()); + } + if item.set(scope, description_key.into(), description.into()) != Some(true) { + return Err("failed to set ALL_TOOLS description".to_string()); + } + if array.set_index(scope, index as u32, item.into()) != Some(true) { + return Err("failed to append ALL_TOOLS metadata".to_string()); + } + } + + Ok(array.into()) +} + +fn helper_function<'s, F>( + scope: &mut v8::PinScope<'s, '_>, + name: &str, + callback: F, +) -> Result, String> +where + F: v8::MapFnTo, +{ + let name = + v8::String::new(scope, name).ok_or_else(|| "failed to allocate helper name".to_string())?; + let template = v8::FunctionTemplate::builder(callback) + .data(name.into()) + .build(scope); + template + .get_function(scope) + .ok_or_else(|| "failed to create helper function".to_string()) +} + +fn tool_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + tool_name: &str, +) -> Result, String> { + let data = v8::String::new(scope, tool_name) + .ok_or_else(|| "failed to allocate tool callback data".to_string())?; + let template = v8::FunctionTemplate::builder(tool_callback) + .data(data.into()) + .build(scope); + template + .get_function(scope) + .ok_or_else(|| "failed to create tool function".to_string()) +} + +fn set_global<'s>( + scope: &mut v8::PinScope<'s, '_>, + global: v8::Local<'s, v8::Object>, + name: &str, + value: v8::Local<'s, v8::Value>, +) -> Result<(), String> { + let key = v8::String::new(scope, name) + .ok_or_else(|| format!("failed to allocate global `{name}`"))?; + if global.set(scope, key.into(), value) == Some(true) { + Ok(()) + } else { + Err(format!("failed to set global `{name}`")) + } +} diff --git a/codex-rs/code-mode/src/runtime/mod.rs b/codex-rs/code-mode/src/runtime/mod.rs new file mode 100644 index 0000000000..df90eda673 --- /dev/null +++ b/codex-rs/code-mode/src/runtime/mod.rs @@ -0,0 +1,349 @@ +mod callbacks; +mod globals; +mod module_loader; +mod value; + +use std::collections::HashMap; +use std::sync::OnceLock; +use std::sync::mpsc as std_mpsc; +use std::thread; + +use serde_json::Value as JsonValue; +use tokio::sync::mpsc; + +use crate::description::EnabledToolMetadata; +use crate::description::ToolDefinition; +use crate::description::enabled_tool_metadata; +use crate::response::FunctionCallOutputContentItem; + +pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000; +pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000; +pub const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL: usize = 10_000; +const EXIT_SENTINEL: &str = "__codex_code_mode_exit__"; + +#[derive(Clone, Debug)] +pub struct ExecuteRequest { + pub tool_call_id: String, + pub enabled_tools: Vec, + pub source: String, + pub stored_values: HashMap, + pub yield_time_ms: Option, + pub max_output_tokens: Option, +} + +#[derive(Clone, Debug)] +pub struct WaitRequest { + pub cell_id: String, + pub yield_time_ms: u64, + pub terminate: bool, +} + +#[derive(Debug, PartialEq)] +pub enum RuntimeResponse { + Yielded { + cell_id: String, + content_items: Vec, + }, + Terminated { + cell_id: String, + content_items: Vec, + }, + Result { + cell_id: String, + content_items: Vec, + stored_values: HashMap, + error_text: Option, + }, +} + +#[derive(Debug)] +pub(crate) enum TurnMessage { + ToolCall { + cell_id: String, + id: String, + name: String, + input: Option, + }, + Notify { + cell_id: String, + call_id: String, + text: String, + }, +} + +#[derive(Debug)] +pub(crate) enum RuntimeCommand { + ToolResponse { id: String, result: JsonValue }, + ToolError { id: String, error_text: String }, + Terminate, +} + +#[derive(Debug)] +pub(crate) enum RuntimeEvent { + Started, + ContentItem(FunctionCallOutputContentItem), + YieldRequested, + ToolCall { + id: String, + name: String, + input: Option, + }, + Notify { + call_id: String, + text: String, + }, + Result { + stored_values: HashMap, + error_text: Option, + }, +} + +pub(crate) fn spawn_runtime( + request: ExecuteRequest, + event_tx: mpsc::UnboundedSender, +) -> Result<(std_mpsc::Sender, v8::IsolateHandle), String> { + let (command_tx, command_rx) = std_mpsc::channel(); + let (isolate_handle_tx, isolate_handle_rx) = std_mpsc::sync_channel(1); + let enabled_tools = request + .enabled_tools + .iter() + .map(enabled_tool_metadata) + .collect::>(); + let config = RuntimeConfig { + tool_call_id: request.tool_call_id, + enabled_tools, + source: request.source, + stored_values: request.stored_values, + }; + + thread::spawn(move || { + run_runtime(config, event_tx, command_rx, isolate_handle_tx); + }); + + let isolate_handle = isolate_handle_rx + .recv() + .map_err(|_| "failed to initialize code mode runtime".to_string())?; + Ok((command_tx, isolate_handle)) +} + +#[derive(Clone)] +struct RuntimeConfig { + tool_call_id: String, + enabled_tools: Vec, + source: String, + stored_values: HashMap, +} + +pub(super) struct RuntimeState { + event_tx: mpsc::UnboundedSender, + pending_tool_calls: HashMap>, + stored_values: HashMap, + enabled_tools: Vec, + next_tool_call_id: u64, + tool_call_id: String, + exit_requested: bool, +} + +pub(super) enum CompletionState { + Pending, + Completed { + stored_values: HashMap, + error_text: Option, + }, +} + +fn initialize_v8() { + static PLATFORM: OnceLock> = OnceLock::new(); + + let _ = PLATFORM.get_or_init(|| { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform.clone()); + v8::V8::initialize(); + platform + }); +} + +fn run_runtime( + config: RuntimeConfig, + event_tx: mpsc::UnboundedSender, + command_rx: std_mpsc::Receiver, + isolate_handle_tx: std_mpsc::SyncSender, +) { + initialize_v8(); + + let isolate = &mut v8::Isolate::new(v8::CreateParams::default()); + let isolate_handle = isolate.thread_safe_handle(); + if isolate_handle_tx.send(isolate_handle).is_err() { + return; + } + isolate.set_host_import_module_dynamically_callback(module_loader::dynamic_import_callback); + + v8::scope!(let scope, isolate); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + + scope.set_slot(RuntimeState { + event_tx: event_tx.clone(), + pending_tool_calls: HashMap::new(), + stored_values: config.stored_values, + enabled_tools: config.enabled_tools, + next_tool_call_id: 1, + tool_call_id: config.tool_call_id, + exit_requested: false, + }); + + if let Err(error_text) = globals::install_globals(scope) { + send_result(&event_tx, HashMap::new(), Some(error_text)); + return; + } + + let _ = event_tx.send(RuntimeEvent::Started); + + let pending_promise = match module_loader::evaluate_main_module(scope, &config.source) { + Ok(pending_promise) => pending_promise, + Err(error_text) => { + capture_scope_send_error(scope, &event_tx, Some(error_text)); + return; + } + }; + + match module_loader::completion_state(scope, pending_promise.as_ref()) { + CompletionState::Completed { + stored_values, + error_text, + } => { + send_result(&event_tx, stored_values, error_text); + return; + } + CompletionState::Pending => {} + } + + let mut pending_promise = pending_promise; + loop { + let Ok(command) = command_rx.recv() else { + break; + }; + match command { + RuntimeCommand::Terminate => break, + RuntimeCommand::ToolResponse { id, result } => { + if let Err(error_text) = + module_loader::resolve_tool_response(scope, &id, Ok(result)) + { + capture_scope_send_error(scope, &event_tx, Some(error_text)); + return; + } + } + RuntimeCommand::ToolError { id, error_text } => { + if let Err(runtime_error) = + module_loader::resolve_tool_response(scope, &id, Err(error_text)) + { + capture_scope_send_error(scope, &event_tx, Some(runtime_error)); + return; + } + } + } + + scope.perform_microtask_checkpoint(); + match module_loader::completion_state(scope, pending_promise.as_ref()) { + CompletionState::Completed { + stored_values, + error_text, + } => { + send_result(&event_tx, stored_values, error_text); + return; + } + CompletionState::Pending => {} + } + + if let Some(promise) = pending_promise.as_ref() { + let promise = v8::Local::new(scope, promise); + if promise.state() != v8::PromiseState::Pending { + pending_promise = None; + } + } + } +} + +fn capture_scope_send_error( + scope: &mut v8::PinScope<'_, '_>, + event_tx: &mpsc::UnboundedSender, + error_text: Option, +) { + let stored_values = scope + .get_slot::() + .map(|state| state.stored_values.clone()) + .unwrap_or_default(); + + send_result(event_tx, stored_values, error_text); +} + +fn send_result( + event_tx: &mpsc::UnboundedSender, + stored_values: HashMap, + error_text: Option, +) { + let _ = event_tx.send(RuntimeEvent::Result { + stored_values, + error_text, + }); +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::time::Duration; + + use pretty_assertions::assert_eq; + use tokio::sync::mpsc; + + use super::ExecuteRequest; + use super::RuntimeEvent; + use super::spawn_runtime; + + fn execute_request(source: &str) -> ExecuteRequest { + ExecuteRequest { + tool_call_id: "call_1".to_string(), + enabled_tools: Vec::new(), + source: source.to_string(), + stored_values: HashMap::new(), + yield_time_ms: Some(1), + max_output_tokens: None, + } + } + + #[tokio::test] + async fn terminate_execution_stops_cpu_bound_module() { + let (event_tx, mut event_rx) = mpsc::unbounded_channel(); + let (_runtime_tx, runtime_terminate_handle) = + spawn_runtime(execute_request("while (true) {}"), event_tx).unwrap(); + + let started_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv()) + .await + .unwrap() + .unwrap(); + assert!(matches!(started_event, RuntimeEvent::Started)); + + assert!(runtime_terminate_handle.terminate_execution()); + + let result_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv()) + .await + .unwrap() + .unwrap(); + let RuntimeEvent::Result { + stored_values, + error_text, + } = result_event + else { + panic!("expected runtime result after termination"); + }; + assert_eq!(stored_values, HashMap::new()); + assert!(error_text.is_some()); + + assert!( + tokio::time::timeout(Duration::from_secs(1), event_rx.recv()) + .await + .unwrap() + .is_none() + ); + } +} diff --git a/codex-rs/code-mode/src/runtime/module_loader.rs b/codex-rs/code-mode/src/runtime/module_loader.rs new file mode 100644 index 0000000000..83ce3d347a --- /dev/null +++ b/codex-rs/code-mode/src/runtime/module_loader.rs @@ -0,0 +1,235 @@ +use serde_json::Value as JsonValue; + +use super::CompletionState; +use super::EXIT_SENTINEL; +use super::RuntimeState; +use super::value::json_to_v8; +use super::value::value_to_error_text; + +pub(super) fn evaluate_main_module( + scope: &mut v8::PinScope<'_, '_>, + source_text: &str, +) -> Result>, String> { + let tc = std::pin::pin!(v8::TryCatch::new(scope)); + let mut tc = tc.init(); + let source = v8::String::new(&tc, source_text) + .ok_or_else(|| "failed to allocate exec source".to_string())?; + let origin = script_origin(&mut tc, "exec_main.mjs")?; + let mut source = v8::script_compiler::Source::new(source, Some(&origin)); + let module = v8::script_compiler::compile_module(&tc, &mut source).ok_or_else(|| { + tc.exception() + .map(|exception| value_to_error_text(&mut tc, exception)) + .unwrap_or_else(|| "unknown code mode exception".to_string()) + })?; + module + .instantiate_module(&tc, resolve_module_callback) + .ok_or_else(|| { + tc.exception() + .map(|exception| value_to_error_text(&mut tc, exception)) + .unwrap_or_else(|| "unknown code mode exception".to_string()) + })?; + let result = match module.evaluate(&tc) { + Some(result) => result, + None => { + if let Some(exception) = tc.exception() { + if is_exit_exception(&mut tc, exception) { + return Ok(None); + } + return Err(value_to_error_text(&mut tc, exception)); + } + return Err("unknown code mode exception".to_string()); + } + }; + tc.perform_microtask_checkpoint(); + + if result.is_promise() { + let promise = v8::Local::::try_from(result) + .map_err(|_| "failed to read exec promise".to_string())?; + return Ok(Some(v8::Global::new(&tc, promise))); + } + + Ok(None) +} + +fn is_exit_exception( + scope: &mut v8::PinScope<'_, '_>, + exception: v8::Local<'_, v8::Value>, +) -> bool { + scope + .get_slot::() + .map(|state| state.exit_requested) + .unwrap_or(false) + && exception.is_string() + && exception.to_rust_string_lossy(scope) == EXIT_SENTINEL +} + +pub(super) fn resolve_tool_response( + scope: &mut v8::PinScope<'_, '_>, + id: &str, + response: Result, +) -> Result<(), String> { + let resolver = { + let state = scope + .get_slot_mut::() + .ok_or_else(|| "runtime state unavailable".to_string())?; + state.pending_tool_calls.remove(id) + } + .ok_or_else(|| format!("unknown tool call `{id}`"))?; + + let tc = std::pin::pin!(v8::TryCatch::new(scope)); + let mut tc = tc.init(); + let resolver = v8::Local::new(&tc, &resolver); + match response { + Ok(result) => { + let value = json_to_v8(&mut tc, &result) + .ok_or_else(|| "failed to serialize tool response".to_string())?; + resolver.resolve(&tc, value); + } + Err(error_text) => { + let value = v8::String::new(&tc, &error_text) + .ok_or_else(|| "failed to allocate tool error".to_string())?; + resolver.reject(&tc, value.into()); + } + } + if tc.has_caught() { + return Err(tc + .exception() + .map(|exception| value_to_error_text(&mut tc, exception)) + .unwrap_or_else(|| "unknown code mode exception".to_string())); + } + Ok(()) +} + +pub(super) fn completion_state( + scope: &mut v8::PinScope<'_, '_>, + pending_promise: Option<&v8::Global>, +) -> CompletionState { + let stored_values = scope + .get_slot::() + .map(|state| state.stored_values.clone()) + .unwrap_or_default(); + + let Some(pending_promise) = pending_promise else { + return CompletionState::Completed { + stored_values, + error_text: None, + }; + }; + + let promise = v8::Local::new(scope, pending_promise); + match promise.state() { + v8::PromiseState::Pending => CompletionState::Pending, + v8::PromiseState::Fulfilled => CompletionState::Completed { + stored_values, + error_text: None, + }, + v8::PromiseState::Rejected => { + let result = promise.result(scope); + let error_text = if is_exit_exception(scope, result) { + None + } else { + Some(value_to_error_text(scope, result)) + }; + CompletionState::Completed { + stored_values, + error_text, + } + } + } +} + +fn script_origin<'s>( + scope: &mut v8::PinScope<'s, '_>, + resource_name_: &str, +) -> Result, String> { + let resource_name = v8::String::new(scope, resource_name_) + .ok_or_else(|| "failed to allocate script origin".to_string())?; + let source_map_url = v8::String::new(scope, resource_name_) + .ok_or_else(|| "failed to allocate source map url".to_string())?; + Ok(v8::ScriptOrigin::new( + scope, + resource_name.into(), + 0, + 0, + true, + 0, + Some(source_map_url.into()), + true, + false, + true, + None, + )) +} + +fn resolve_module_callback<'s>( + context: v8::Local<'s, v8::Context>, + specifier: v8::Local<'s, v8::String>, + _import_attributes: v8::Local<'s, v8::FixedArray>, + _referrer: v8::Local<'s, v8::Module>, +) -> Option> { + v8::callback_scope!(unsafe scope, context); + let specifier = specifier.to_rust_string_lossy(scope); + resolve_module(scope, &specifier) +} + +pub(super) fn dynamic_import_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + _host_defined_options: v8::Local<'s, v8::Data>, + _resource_name: v8::Local<'s, v8::Value>, + specifier: v8::Local<'s, v8::String>, + _import_attributes: v8::Local<'s, v8::FixedArray>, +) -> Option> { + let specifier = specifier.to_rust_string_lossy(scope); + let resolver = v8::PromiseResolver::new(scope)?; + + match resolve_module(scope, &specifier) { + Some(module) => { + if module.get_status() == v8::ModuleStatus::Uninstantiated + && module + .instantiate_module(scope, resolve_module_callback) + .is_none() + { + let error = v8::String::new(scope, "failed to instantiate module") + .map(Into::into) + .unwrap_or_else(|| v8::undefined(scope).into()); + resolver.reject(scope, error); + return Some(resolver.get_promise(scope)); + } + if matches!( + module.get_status(), + v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated + ) && module.evaluate(scope).is_none() + { + let error = v8::String::new(scope, "failed to evaluate module") + .map(Into::into) + .unwrap_or_else(|| v8::undefined(scope).into()); + resolver.reject(scope, error); + return Some(resolver.get_promise(scope)); + } + let namespace = module.get_module_namespace(); + resolver.resolve(scope, namespace); + Some(resolver.get_promise(scope)) + } + None => { + let error = v8::String::new(scope, "unsupported import in exec") + .map(Into::into) + .unwrap_or_else(|| v8::undefined(scope).into()); + resolver.reject(scope, error); + Some(resolver.get_promise(scope)) + } + } +} + +fn resolve_module<'s>( + scope: &mut v8::PinScope<'s, '_>, + specifier: &str, +) -> Option> { + if let Some(message) = + v8::String::new(scope, &format!("Unsupported import in exec: {specifier}")) + { + scope.throw_exception(message.into()); + } else { + scope.throw_exception(v8::undefined(scope).into()); + } + None +} diff --git a/codex-rs/code-mode/src/runtime/value.rs b/codex-rs/code-mode/src/runtime/value.rs new file mode 100644 index 0000000000..eb0280142c --- /dev/null +++ b/codex-rs/code-mode/src/runtime/value.rs @@ -0,0 +1,163 @@ +use serde_json::Value as JsonValue; + +use crate::response::FunctionCallOutputContentItem; +use crate::response::ImageDetail; + +pub(super) fn serialize_output_text( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local<'_, v8::Value>, +) -> Result { + if value.is_undefined() + || value.is_null() + || value.is_boolean() + || value.is_number() + || value.is_big_int() + || value.is_string() + { + return Ok(value.to_rust_string_lossy(scope)); + } + + let tc = std::pin::pin!(v8::TryCatch::new(scope)); + let mut tc = tc.init(); + if let Some(stringified) = v8::json::stringify(&tc, value) { + return Ok(stringified.to_rust_string_lossy(&tc)); + } + if tc.has_caught() { + return Err(tc + .exception() + .map(|exception| value_to_error_text(&mut tc, exception)) + .unwrap_or_else(|| "unknown code mode exception".to_string())); + } + Ok(value.to_rust_string_lossy(&tc)) +} + +pub(super) fn normalize_output_image( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local<'_, v8::Value>, +) -> Result { + let result = (|| -> Result { + let (image_url, detail) = if value.is_string() { + (value.to_rust_string_lossy(scope), None) + } else if value.is_object() && !value.is_array() { + let object = v8::Local::::try_from(value).map_err(|_| { + "image expects a non-empty image URL string or an object with image_url and optional detail".to_string() + })?; + let image_url_key = v8::String::new(scope, "image_url") + .ok_or_else(|| "failed to allocate image helper keys".to_string())?; + let detail_key = v8::String::new(scope, "detail") + .ok_or_else(|| "failed to allocate image helper keys".to_string())?; + let image_url = object + .get(scope, image_url_key.into()) + .filter(|value| value.is_string()) + .map(|value| value.to_rust_string_lossy(scope)) + .ok_or_else(|| { + "image expects a non-empty image URL string or an object with image_url and optional detail" + .to_string() + })?; + let detail = match object.get(scope, detail_key.into()) { + Some(value) if value.is_string() => Some(value.to_rust_string_lossy(scope)), + Some(value) if value.is_null() || value.is_undefined() => None, + Some(_) => return Err("image detail must be a string when provided".to_string()), + None => None, + }; + (image_url, detail) + } else { + return Err( + "image expects a non-empty image URL string or an object with image_url and optional detail" + .to_string(), + ); + }; + + if image_url.is_empty() { + return Err( + "image expects a non-empty image URL string or an object with image_url and optional detail" + .to_string(), + ); + } + let lower = image_url.to_ascii_lowercase(); + if !(lower.starts_with("http://") + || lower.starts_with("https://") + || lower.starts_with("data:")) + { + return Err("image expects an http(s) or data URL".to_string()); + } + + let detail = match detail { + Some(detail) => { + let normalized = detail.to_ascii_lowercase(); + Some(match normalized.as_str() { + "auto" => ImageDetail::Auto, + "low" => ImageDetail::Low, + "high" => ImageDetail::High, + "original" => ImageDetail::Original, + _ => { + return Err( + "image detail must be one of: auto, low, high, original".to_string() + ); + } + }) + } + None => None, + }; + + Ok(FunctionCallOutputContentItem::InputImage { image_url, detail }) + })(); + + match result { + Ok(item) => Ok(item), + Err(error_text) => { + throw_type_error(scope, &error_text); + Err(()) + } + } +} + +pub(super) fn v8_value_to_json( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local<'_, v8::Value>, +) -> Result, String> { + let tc = std::pin::pin!(v8::TryCatch::new(scope)); + let mut tc = tc.init(); + let Some(stringified) = v8::json::stringify(&tc, value) else { + if tc.has_caught() { + return Err(tc + .exception() + .map(|exception| value_to_error_text(&mut tc, exception)) + .unwrap_or_else(|| "unknown code mode exception".to_string())); + } + return Ok(None); + }; + serde_json::from_str(&stringified.to_rust_string_lossy(&tc)) + .map(Some) + .map_err(|err| format!("failed to serialize JavaScript value: {err}")) +} + +pub(super) fn json_to_v8<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: &JsonValue, +) -> Option> { + let json = serde_json::to_string(value).ok()?; + let json = v8::String::new(scope, &json)?; + v8::json::parse(scope, json) +} + +pub(super) fn value_to_error_text( + scope: &mut v8::PinScope<'_, '_>, + value: v8::Local<'_, v8::Value>, +) -> String { + if value.is_object() + && let Ok(object) = v8::Local::::try_from(value) + && let Some(key) = v8::String::new(scope, "stack") + && let Some(stack) = object.get(scope, key.into()) + && stack.is_string() + { + return stack.to_rust_string_lossy(scope); + } + value.to_rust_string_lossy(scope) +} + +pub(super) fn throw_type_error(scope: &mut v8::PinScope<'_, '_>, message: &str) { + if let Some(message) = v8::String::new(scope, message) { + scope.throw_exception(message.into()); + } +} diff --git a/codex-rs/code-mode/src/service.rs b/codex-rs/code-mode/src/service.rs new file mode 100644 index 0000000000..260b891d36 --- /dev/null +++ b/codex-rs/code-mode/src/service.rs @@ -0,0 +1,673 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use async_trait::async_trait; +use serde_json::Value as JsonValue; +use tokio::sync::Mutex; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_util::sync::CancellationToken; +use tracing::warn; + +use crate::FunctionCallOutputContentItem; +use crate::runtime::DEFAULT_EXEC_YIELD_TIME_MS; +use crate::runtime::ExecuteRequest; +use crate::runtime::RuntimeCommand; +use crate::runtime::RuntimeEvent; +use crate::runtime::RuntimeResponse; +use crate::runtime::TurnMessage; +use crate::runtime::WaitRequest; +use crate::runtime::spawn_runtime; + +#[async_trait] +pub trait CodeModeTurnHost: Send + Sync { + async fn invoke_tool( + &self, + tool_name: String, + input: Option, + cancellation_token: CancellationToken, + ) -> Result; + + async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>; +} + +#[derive(Clone)] +struct SessionHandle { + control_tx: mpsc::UnboundedSender, + runtime_tx: std::sync::mpsc::Sender, +} + +struct Inner { + stored_values: Mutex>, + sessions: Mutex>, + turn_message_tx: mpsc::UnboundedSender, + turn_message_rx: Arc>>, + next_cell_id: AtomicU64, +} + +pub struct CodeModeService { + inner: Arc, +} + +impl CodeModeService { + pub fn new() -> Self { + let (turn_message_tx, turn_message_rx) = mpsc::unbounded_channel(); + + Self { + inner: Arc::new(Inner { + stored_values: Mutex::new(HashMap::new()), + sessions: Mutex::new(HashMap::new()), + turn_message_tx, + turn_message_rx: Arc::new(Mutex::new(turn_message_rx)), + next_cell_id: AtomicU64::new(1), + }), + } + } + + pub async fn stored_values(&self) -> HashMap { + self.inner.stored_values.lock().await.clone() + } + + pub async fn replace_stored_values(&self, values: HashMap) { + *self.inner.stored_values.lock().await = values; + } + + pub async fn execute(&self, request: ExecuteRequest) -> Result { + let cell_id = self + .inner + .next_cell_id + .fetch_add(1, Ordering::Relaxed) + .to_string(); + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let (runtime_tx, runtime_terminate_handle) = spawn_runtime(request.clone(), event_tx)?; + let (control_tx, control_rx) = mpsc::unbounded_channel(); + let (response_tx, response_rx) = oneshot::channel(); + + self.inner.sessions.lock().await.insert( + cell_id.clone(), + SessionHandle { + control_tx: control_tx.clone(), + runtime_tx: runtime_tx.clone(), + }, + ); + + tokio::spawn(run_session_control( + Arc::clone(&self.inner), + SessionControlContext { + cell_id: cell_id.clone(), + runtime_tx, + runtime_terminate_handle, + }, + event_rx, + control_rx, + response_tx, + request.yield_time_ms.unwrap_or(DEFAULT_EXEC_YIELD_TIME_MS), + )); + + response_rx + .await + .map_err(|_| "exec runtime ended unexpectedly".to_string()) + } + + pub async fn wait(&self, request: WaitRequest) -> Result { + let cell_id = request.cell_id.clone(); + let handle = self + .inner + .sessions + .lock() + .await + .get(&request.cell_id) + .cloned(); + let Some(handle) = handle else { + return Ok(missing_cell_response(cell_id)); + }; + let (response_tx, response_rx) = oneshot::channel(); + let control_message = if request.terminate { + SessionControlCommand::Terminate { response_tx } + } else { + SessionControlCommand::Poll { + yield_time_ms: request.yield_time_ms, + response_tx, + } + }; + if handle.control_tx.send(control_message).is_err() { + return Ok(missing_cell_response(cell_id)); + } + match response_rx.await { + Ok(response) => Ok(response), + Err(_) => Ok(missing_cell_response(request.cell_id)), + } + } + + pub fn start_turn_worker(&self, host: Arc) -> CodeModeTurnWorker { + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + let inner = Arc::clone(&self.inner); + let turn_message_rx = Arc::clone(&self.inner.turn_message_rx); + + tokio::spawn(async move { + loop { + let next_message = tokio::select! { + _ = &mut shutdown_rx => break, + message = async { + let mut turn_message_rx = turn_message_rx.lock().await; + turn_message_rx.recv().await + } => message, + }; + let Some(next_message) = next_message else { + break; + }; + match next_message { + TurnMessage::Notify { + cell_id, + call_id, + text, + } => { + if let Err(err) = host.notify(call_id, cell_id.clone(), text).await { + warn!( + "failed to deliver code mode notification for cell {cell_id}: {err}" + ); + } + } + TurnMessage::ToolCall { + cell_id, + id, + name, + input, + } => { + let host = Arc::clone(&host); + let inner = Arc::clone(&inner); + tokio::spawn(async move { + let response = host + .invoke_tool(name, input, CancellationToken::new()) + .await; + let runtime_tx = inner + .sessions + .lock() + .await + .get(&cell_id) + .map(|handle| handle.runtime_tx.clone()); + let Some(runtime_tx) = runtime_tx else { + return; + }; + let command = match response { + Ok(result) => RuntimeCommand::ToolResponse { id, result }, + Err(error_text) => RuntimeCommand::ToolError { id, error_text }, + }; + let _ = runtime_tx.send(command); + }); + } + } + } + }); + + CodeModeTurnWorker { + shutdown_tx: Some(shutdown_tx), + } + } +} + +impl Default for CodeModeService { + fn default() -> Self { + Self::new() + } +} + +pub struct CodeModeTurnWorker { + shutdown_tx: Option>, +} + +impl Drop for CodeModeTurnWorker { + fn drop(&mut self) { + if let Some(shutdown_tx) = self.shutdown_tx.take() { + let _ = shutdown_tx.send(()); + } + } +} + +enum SessionControlCommand { + Poll { + yield_time_ms: u64, + response_tx: oneshot::Sender, + }, + Terminate { + response_tx: oneshot::Sender, + }, +} + +struct PendingResult { + content_items: Vec, + stored_values: HashMap, + error_text: Option, +} + +struct SessionControlContext { + cell_id: String, + runtime_tx: std::sync::mpsc::Sender, + runtime_terminate_handle: v8::IsolateHandle, +} + +fn missing_cell_response(cell_id: String) -> RuntimeResponse { + RuntimeResponse::Result { + error_text: Some(format!("exec cell {cell_id} not found")), + cell_id, + content_items: Vec::new(), + stored_values: HashMap::new(), + } +} + +fn pending_result_response(cell_id: &str, result: PendingResult) -> RuntimeResponse { + RuntimeResponse::Result { + cell_id: cell_id.to_string(), + content_items: result.content_items, + stored_values: result.stored_values, + error_text: result.error_text, + } +} + +fn send_or_buffer_result( + cell_id: &str, + result: PendingResult, + response_tx: &mut Option>, + pending_result: &mut Option, +) -> bool { + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(pending_result_response(cell_id, result)); + return true; + } + + *pending_result = Some(result); + false +} + +async fn run_session_control( + inner: Arc, + context: SessionControlContext, + mut event_rx: mpsc::UnboundedReceiver, + mut control_rx: mpsc::UnboundedReceiver, + initial_response_tx: oneshot::Sender, + initial_yield_time_ms: u64, +) { + let SessionControlContext { + cell_id, + runtime_tx, + runtime_terminate_handle, + } = context; + let mut content_items = Vec::new(); + let mut pending_result: Option = None; + let mut response_tx = Some(initial_response_tx); + let mut termination_requested = false; + let mut runtime_closed = false; + let mut yield_timer: Option>> = None; + + loop { + tokio::select! { + maybe_event = async { + if runtime_closed { + std::future::pending::>().await + } else { + event_rx.recv().await + } + } => { + let Some(event) = maybe_event else { + runtime_closed = true; + if termination_requested { + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(RuntimeResponse::Terminated { + cell_id: cell_id.clone(), + content_items: std::mem::take(&mut content_items), + }); + } + break; + } + if pending_result.is_none() { + let result = PendingResult { + content_items: std::mem::take(&mut content_items), + stored_values: HashMap::new(), + error_text: Some("exec runtime ended unexpectedly".to_string()), + }; + if send_or_buffer_result( + &cell_id, + result, + &mut response_tx, + &mut pending_result, + ) { + break; + } + } + continue; + }; + match event { + RuntimeEvent::Started => { + yield_timer = Some(Box::pin(tokio::time::sleep(Duration::from_millis(initial_yield_time_ms)))); + } + RuntimeEvent::ContentItem(item) => { + content_items.push(item); + } + RuntimeEvent::YieldRequested => { + yield_timer = None; + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(RuntimeResponse::Yielded { + cell_id: cell_id.clone(), + content_items: std::mem::take(&mut content_items), + }); + } + } + RuntimeEvent::Notify { call_id, text } => { + let _ = inner.turn_message_tx.send(TurnMessage::Notify { + cell_id: cell_id.clone(), + call_id, + text, + }); + } + RuntimeEvent::ToolCall { id, name, input } => { + let _ = inner.turn_message_tx.send(TurnMessage::ToolCall { + cell_id: cell_id.clone(), + id, + name, + input, + }); + } + RuntimeEvent::Result { + stored_values, + error_text, + } => { + yield_timer = None; + if termination_requested { + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(RuntimeResponse::Terminated { + cell_id: cell_id.clone(), + content_items: std::mem::take(&mut content_items), + }); + } + break; + } + let result = PendingResult { + content_items: std::mem::take(&mut content_items), + stored_values, + error_text, + }; + if send_or_buffer_result( + &cell_id, + result, + &mut response_tx, + &mut pending_result, + ) { + break; + } + } + } + } + maybe_command = control_rx.recv() => { + let Some(command) = maybe_command else { + break; + }; + match command { + SessionControlCommand::Poll { + yield_time_ms, + response_tx: next_response_tx, + } => { + if let Some(result) = pending_result.take() { + let _ = next_response_tx.send(pending_result_response(&cell_id, result)); + break; + } + response_tx = Some(next_response_tx); + yield_timer = Some(Box::pin(tokio::time::sleep(Duration::from_millis(yield_time_ms)))); + } + SessionControlCommand::Terminate { response_tx: next_response_tx } => { + if let Some(result) = pending_result.take() { + let _ = next_response_tx.send(pending_result_response(&cell_id, result)); + break; + } + + response_tx = Some(next_response_tx); + termination_requested = true; + yield_timer = None; + let _ = runtime_tx.send(RuntimeCommand::Terminate); + let _ = runtime_terminate_handle.terminate_execution(); + if runtime_closed { + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(RuntimeResponse::Terminated { + cell_id: cell_id.clone(), + content_items: std::mem::take(&mut content_items), + }); + } + break; + } else { + continue; + } + } + } + } + _ = async { + if let Some(yield_timer) = yield_timer.as_mut() { + yield_timer.await; + } else { + std::future::pending::<()>().await; + } + } => { + yield_timer = None; + if let Some(response_tx) = response_tx.take() { + let _ = response_tx.send(RuntimeResponse::Yielded { + cell_id: cell_id.clone(), + content_items: std::mem::take(&mut content_items), + }); + } + } + } + } + + let _ = runtime_tx.send(RuntimeCommand::Terminate); + inner.sessions.lock().await.remove(&cell_id); +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::Arc; + use std::sync::atomic::AtomicU64; + use std::time::Duration; + + use pretty_assertions::assert_eq; + use tokio::sync::Mutex; + use tokio::sync::mpsc; + use tokio::sync::oneshot; + + use super::CodeModeService; + use super::Inner; + use super::RuntimeCommand; + use super::RuntimeResponse; + use super::SessionControlCommand; + use super::SessionControlContext; + use super::run_session_control; + use crate::FunctionCallOutputContentItem; + use crate::runtime::ExecuteRequest; + use crate::runtime::RuntimeEvent; + use crate::runtime::spawn_runtime; + + fn execute_request(source: &str) -> ExecuteRequest { + ExecuteRequest { + tool_call_id: "call_1".to_string(), + enabled_tools: Vec::new(), + source: source.to_string(), + stored_values: HashMap::new(), + yield_time_ms: Some(1), + max_output_tokens: None, + } + } + + fn test_inner() -> Arc { + let (turn_message_tx, turn_message_rx) = mpsc::unbounded_channel(); + Arc::new(Inner { + stored_values: Mutex::new(HashMap::new()), + sessions: Mutex::new(HashMap::new()), + turn_message_tx, + turn_message_rx: Arc::new(Mutex::new(turn_message_rx)), + next_cell_id: AtomicU64::new(1), + }) + } + + #[tokio::test] + async fn synchronous_exit_returns_successfully() { + let service = CodeModeService::new(); + + let response = service + .execute(ExecuteRequest { + source: r#"text("before"); exit(); text("after");"#.to_string(), + yield_time_ms: None, + ..execute_request("") + }) + .await + .unwrap(); + + assert_eq!( + response, + RuntimeResponse::Result { + cell_id: "1".to_string(), + content_items: vec![FunctionCallOutputContentItem::InputText { + text: "before".to_string(), + }], + stored_values: HashMap::new(), + error_text: None, + } + ); + } + + #[tokio::test] + async fn v8_console_is_not_exposed_on_global_this() { + let service = CodeModeService::new(); + + let response = service + .execute(ExecuteRequest { + source: r#"text(String(Object.hasOwn(globalThis, "console")));"#.to_string(), + yield_time_ms: None, + ..execute_request("") + }) + .await + .unwrap(); + + assert_eq!( + response, + RuntimeResponse::Result { + cell_id: "1".to_string(), + content_items: vec![FunctionCallOutputContentItem::InputText { + text: "false".to_string(), + }], + stored_values: HashMap::new(), + error_text: None, + } + ); + } + + #[tokio::test] + async fn output_helpers_return_undefined() { + let service = CodeModeService::new(); + + let response = service + .execute(ExecuteRequest { + source: r#" +const returnsUndefined = [ + text("first"), + image("https://example.com/image.jpg"), + notify("ping"), +].map((value) => value === undefined); +text(JSON.stringify(returnsUndefined)); +"# + .to_string(), + yield_time_ms: None, + ..execute_request("") + }) + .await + .unwrap(); + + assert_eq!( + response, + RuntimeResponse::Result { + cell_id: "1".to_string(), + content_items: vec![ + FunctionCallOutputContentItem::InputText { + text: "first".to_string(), + }, + FunctionCallOutputContentItem::InputImage { + image_url: "https://example.com/image.jpg".to_string(), + detail: None, + }, + FunctionCallOutputContentItem::InputText { + text: "[true,true,true]".to_string(), + }, + ], + stored_values: HashMap::new(), + error_text: None, + } + ); + } + + #[tokio::test] + async fn terminate_waits_for_runtime_shutdown_before_responding() { + let inner = test_inner(); + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let (control_tx, control_rx) = mpsc::unbounded_channel(); + let (initial_response_tx, initial_response_rx) = oneshot::channel(); + let (runtime_event_tx, _runtime_event_rx) = mpsc::unbounded_channel(); + let (runtime_tx, runtime_terminate_handle) = spawn_runtime( + ExecuteRequest { + source: "await new Promise(() => {})".to_string(), + yield_time_ms: None, + ..execute_request("") + }, + runtime_event_tx, + ) + .unwrap(); + + tokio::spawn(run_session_control( + inner, + SessionControlContext { + cell_id: "cell-1".to_string(), + runtime_tx: runtime_tx.clone(), + runtime_terminate_handle, + }, + event_rx, + control_rx, + initial_response_tx, + 60_000, + )); + + event_tx.send(RuntimeEvent::Started).unwrap(); + event_tx.send(RuntimeEvent::YieldRequested).unwrap(); + assert_eq!( + initial_response_rx.await.unwrap(), + RuntimeResponse::Yielded { + cell_id: "cell-1".to_string(), + content_items: Vec::new(), + } + ); + + let (terminate_response_tx, terminate_response_rx) = oneshot::channel(); + control_tx + .send(SessionControlCommand::Terminate { + response_tx: terminate_response_tx, + }) + .unwrap(); + let terminate_response = async { terminate_response_rx.await.unwrap() }; + tokio::pin!(terminate_response); + assert!( + tokio::time::timeout(Duration::from_millis(100), terminate_response.as_mut()) + .await + .is_err() + ); + + drop(event_tx); + + assert_eq!( + terminate_response.await, + RuntimeResponse::Terminated { + cell_id: "cell-1".to_string(), + content_items: Vec::new(), + } + ); + + let _ = runtime_tx.send(RuntimeCommand::Terminate); + } +} diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index d648655b24..6386c8d424 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -31,6 +31,7 @@ codex-api = { workspace = true } codex-app-server-protocol = { workspace = true } codex-apply-patch = { workspace = true } codex-async-utils = { workspace = true } +codex-code-mode = { workspace = true } codex-connectors = { workspace = true } codex-config = { workspace = true } codex-exec-server = { workspace = true } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index ba774380df..009ef57369 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -431,9 +431,6 @@ "plugins": { "type": "boolean" }, - "powershell_utf8": { - "type": "boolean" - }, "prevent_idle_sleep": { "type": "boolean" }, @@ -2040,9 +2037,6 @@ "plugins": { "type": "boolean" }, - "powershell_utf8": { - "type": "boolean" - }, "prevent_idle_sleep": { "type": "boolean" }, diff --git a/codex-rs/core/src/arc_monitor.rs b/codex-rs/core/src/arc_monitor.rs index c704faafc0..ba9a886de6 100644 --- a/codex-rs/core/src/arc_monitor.rs +++ b/codex-rs/core/src/arc_monitor.rs @@ -99,6 +99,7 @@ pub(crate) async fn monitor_action( sess: &Session, turn_context: &TurnContext, action: serde_json::Value, + protection_client_callsite: &'static str, ) -> ArcMonitorOutcome { let auth = match turn_context.auth_manager.as_ref() { Some(auth_manager) => match auth_manager.auth().await { @@ -138,7 +139,8 @@ pub(crate) async fn monitor_action( return ArcMonitorOutcome::Ok; } }; - let body = build_arc_monitor_request(sess, turn_context, action).await; + let body = + build_arc_monitor_request(sess, turn_context, action, protection_client_callsite).await; let client = build_reqwest_client(); let mut request = client .post(&url) @@ -236,6 +238,7 @@ async fn build_arc_monitor_request( sess: &Session, turn_context: &TurnContext, action: serde_json::Map, + protection_client_callsite: &'static str, ) -> ArcMonitorRequest { let history = sess.clone_history().await; let mut messages = build_arc_monitor_messages(history.raw_items()); @@ -254,7 +257,7 @@ async fn build_arc_monitor_request( codex_thread_id: conversation_id.clone(), codex_turn_id: turn_context.sub_id.clone(), conversation_id: Some(conversation_id), - protection_client_callsite: None, + protection_client_callsite: Some(protection_client_callsite.to_string()), }, messages: Some(messages), input: None, diff --git a/codex-rs/core/src/arc_monitor_tests.rs b/codex-rs/core/src/arc_monitor_tests.rs index 0b5cdf3029..ab88fddca9 100644 --- a/codex-rs/core/src/arc_monitor_tests.rs +++ b/codex-rs/core/src/arc_monitor_tests.rs @@ -178,6 +178,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies() &turn_context, serde_json::from_value(serde_json::json!({ "tool": "mcp_tool_call" })) .expect("action should deserialize"), + "normal", ) .await; @@ -188,7 +189,7 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies() codex_thread_id: session.conversation_id.to_string(), codex_turn_id: turn_context.sub_id.clone(), conversation_id: Some(session.conversation_id.to_string()), - protection_client_callsite: None, + protection_client_callsite: Some("normal".to_string()), }, messages: Some(vec![ ArcMonitorChatMessage { @@ -285,6 +286,7 @@ async fn monitor_action_posts_expected_arc_request() { "codex_thread_id": session.conversation_id.to_string(), "codex_turn_id": turn_context.sub_id.clone(), "conversation_id": session.conversation_id.to_string(), + "protection_client_callsite": "normal", }, "messages": [{ "role": "user", @@ -320,6 +322,7 @@ async fn monitor_action_posts_expected_arc_request() { &session, &turn_context, serde_json::json!({ "tool": "mcp_tool_call" }), + "normal", ) .await; @@ -377,6 +380,7 @@ async fn monitor_action_uses_env_url_and_token_overrides() { &session, &turn_context, serde_json::json!({ "tool": "mcp_tool_call" }), + "normal", ) .await; @@ -428,6 +432,7 @@ async fn monitor_action_rejects_legacy_response_fields() { &session, &turn_context, serde_json::json!({ "tool": "mcp_tool_call" }), + "normal", ) .await; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 12270bb6dd..cbaabe6b84 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3439,6 +3439,7 @@ impl Session { DeveloperInstructions::from_policy( turn_context.sandbox_policy.get(), turn_context.approval_policy.value(), + turn_context.config.approvals_reviewer, self.services.exec_policy.current().as_ref(), &turn_context.cwd, turn_context @@ -5510,20 +5511,6 @@ pub(crate) async fn run_turn( &available_connectors, &skill_name_counts_lower, )); - // Explicit plugin mentions can make a plugin's enabled apps callable for - // this turn without persisting those connectors as sticky user selections. - let mut turn_enabled_connectors = explicitly_enabled_connectors.clone(); - turn_enabled_connectors.extend( - mentioned_plugins - .iter() - .flat_map(|plugin| plugin.app_connector_ids.iter()) - .map(|connector_id| connector_id.0.clone()) - .filter(|connector_id| { - available_connectors - .iter() - .any(|connector| connector.is_enabled && connector.id == *connector_id) - }), - ); let connector_names_by_id = available_connectors .iter() .map(|connector| (connector.id.as_str(), connector.name.as_str())) @@ -5675,7 +5662,7 @@ pub(crate) async fn run_turn( &mut client_session, turn_metadata_header.as_deref(), sampling_request_input, - &turn_enabled_connectors, + &explicitly_enabled_connectors, skills_outcome, &mut server_model_warning_emitted_for_turn, cancellation_token.child_token(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index a5412eff29..e2ad0982bc 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -64,17 +64,31 @@ use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; use codex_network_proxy::NetworkProxyConfig; use codex_otel::TelemetryAuthMode; +use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::ModeKind; +use codex_protocol::config_types::Settings; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelsResponse; +use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ConversationAudioParams; use codex_protocol::protocol::RealtimeAudioFrame; use codex_protocol::protocol::Submission; use codex_protocol::protocol::W3cTraceContext; +use core_test_support::context_snapshot; +use core_test_support::context_snapshot::ContextSnapshotOptions; +use core_test_support::context_snapshot::ContextSnapshotRenderMode; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::test_codex::test_codex; use core_test_support::tracing::install_test_tracing; +use core_test_support::wait_for_event; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; use std::path::Path; @@ -641,6 +655,21 @@ fn filter_connectors_for_input_skips_disabled_connectors() { assert_eq!(selected, Vec::new()); } +#[test] +fn filter_connectors_for_input_skips_plugin_mentions() { + let connectors = vec![make_connector("figma", "Figma")]; + let input = vec![user_message("use [@figma](plugin://figma@openai-curated)")]; + let explicitly_enabled_connectors = HashSet::new(); + let selected = filter_connectors_for_input( + &connectors, + &input, + &explicitly_enabled_connectors, + &HashMap::new(), + ); + + assert_eq!(selected, Vec::new()); +} + #[test] fn collect_explicit_app_ids_from_skill_items_includes_linked_mentions() { let connectors = vec![make_connector("calendar", "Calendar")]; @@ -1100,6 +1129,111 @@ async fn record_initial_history_reconstructs_forked_transcript() { assert_eq!(expected, history.raw_items()); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<()> { + let server = start_mock_server().await; + mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + let first_forked_request = mount_sse_once( + &server, + sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), + ) + .await; + + let mut builder = test_codex().with_config(|config| { + config.permissions.approval_policy = + codex_config::Constrained::allow_any(AskForApproval::OnRequest); + }); + let initial = builder.build(&server).await?; + let rollout_path = initial + .session_configured + .rollout_path + .clone() + .expect("rollout path"); + + initial + .codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "fork seed".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await?; + wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let mut fork_config = initial.config.clone(); + fork_config.permissions.approval_policy = + codex_config::Constrained::allow_any(AskForApproval::UnlessTrusted); + let forked = initial + .thread_manager + .fork_thread(usize::MAX, fork_config, rollout_path, false, None) + .await?; + + let collaboration_mode = CollaborationMode { + mode: ModeKind::Plan, + settings: Settings { + model: forked.session_configured.model.clone(), + reasoning_effort: None, + developer_instructions: Some("Fork turn collaboration instructions.".to_string()), + }, + }; + forked + .thread + .submit(Op::OverrideTurnContext { + cwd: None, + approval_policy: Some(AskForApproval::Never), + approvals_reviewer: None, + sandbox_policy: None, + windows_sandbox_level: None, + model: None, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: Some(collaboration_mode), + personality: None, + }) + .await?; + + forked + .thread + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "after fork".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await?; + wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let request = first_forked_request.single_request(); + let snapshot = context_snapshot::format_labeled_requests_snapshot( + "First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode.", + &[("First Forked Turn Request", &request)], + &ContextSnapshotOptions::default() + .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 }) + .strip_capability_instructions() + .strip_agents_md_user_context(), + ); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path("snapshots"); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| { + insta::assert_snapshot!( + "codex_core__codex_tests__fork_startup_context_then_first_turn_diff", + snapshot + ); + }); + + Ok(()) +} + #[tokio::test] async fn record_initial_history_forked_hydrates_previous_turn_settings() { let (session, turn_context) = make_session_and_context().await; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1c78759ec4..65c05468e0 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5677,7 +5677,7 @@ approvals_reviewer = "guardian_subagent" } #[tokio::test] -async fn smart_approvals_alias_is_migrated_to_guardian_approval() -> std::io::Result<()> { +async fn smart_approvals_alias_is_ignored() -> std::io::Result<()> { let codex_home = TempDir::new()?; std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -5692,23 +5692,19 @@ smart_approvals = true .build() .await?; - assert!(config.features.enabled(Feature::GuardianApproval)); - assert_eq!(config.features.legacy_feature_usages().count(), 0); - assert_eq!( - config.approvals_reviewer, - ApprovalsReviewer::GuardianSubagent - ); + assert!(!config.features.enabled(Feature::GuardianApproval)); + assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User); let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - assert!(serialized.contains("guardian_approval = true")); - assert!(serialized.contains("approvals_reviewer = \"guardian_subagent\"")); - assert!(!serialized.contains("smart_approvals")); + assert!(serialized.contains("smart_approvals = true")); + assert!(!serialized.contains("guardian_approval")); + assert!(!serialized.contains("approvals_reviewer")); Ok(()) } #[tokio::test] -async fn smart_approvals_alias_is_migrated_in_profiles() -> std::io::Result<()> { +async fn smart_approvals_alias_is_ignored_in_profiles() -> std::io::Result<()> { let codex_home = TempDir::new()?; std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), @@ -5719,106 +5715,6 @@ smart_approvals = true "#, )?; - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .build() - .await?; - - assert!(config.features.enabled(Feature::GuardianApproval)); - assert_eq!(config.features.legacy_feature_usages().count(), 0); - assert_eq!( - config.approvals_reviewer, - ApprovalsReviewer::GuardianSubagent - ); - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - assert!(serialized.contains("[profiles.guardian.features]")); - assert!(serialized.contains("guardian_approval = true")); - assert!(serialized.contains("approvals_reviewer = \"guardian_subagent\"")); - assert!(!serialized.contains("smart_approvals")); - - Ok(()) -} - -#[tokio::test] -async fn smart_approvals_alias_migration_preserves_disabled_profile_override() -> std::io::Result<()> -{ - let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join(CONFIG_TOML_FILE), - r#"[features] -guardian_approval = true - -[profiles.guardian.features] -smart_approvals = false -"#, - )?; - - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .harness_overrides(ConfigOverrides { - config_profile: Some("guardian".to_string()), - ..Default::default() - }) - .build() - .await?; - - assert!(!config.features.enabled(Feature::GuardianApproval)); - assert_eq!(config.features.legacy_feature_usages().count(), 0); - assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User); - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - assert!(serialized.contains("[profiles.guardian.features]")); - assert!(serialized.contains("guardian_approval = false")); - assert!(!serialized.contains("smart_approvals")); - - Ok(()) -} - -#[tokio::test] -async fn smart_approvals_alias_migration_preserves_existing_approvals_reviewer() --> std::io::Result<()> { - let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join(CONFIG_TOML_FILE), - r#"approvals_reviewer = "user" - -[features] -smart_approvals = true -"#, - )?; - - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .build() - .await?; - - assert!(config.features.enabled(Feature::GuardianApproval)); - assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User); - - let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - assert!(serialized.contains("guardian_approval = true")); - assert!(serialized.contains("approvals_reviewer = \"user\"")); - assert!(!serialized.contains("smart_approvals")); - - Ok(()) -} - -#[tokio::test] -async fn smart_approvals_alias_migration_does_not_override_canonical_disabled_flag() --> std::io::Result<()> { - let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join(CONFIG_TOML_FILE), - r#"[features] -guardian_approval = false -smart_approvals = true -"#, - )?; - let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) @@ -5829,9 +5725,10 @@ smart_approvals = true assert_eq!(config.approvals_reviewer, ApprovalsReviewer::User); let serialized = tokio::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).await?; - assert!(serialized.contains("guardian_approval = false")); - assert!(!serialized.contains("approvals_reviewer = \"guardian_subagent\"")); - assert!(!serialized.contains("smart_approvals")); + assert!(serialized.contains("[profiles.guardian.features]")); + assert!(serialized.contains("smart_approvals = true")); + assert!(!serialized.contains("guardian_approval")); + assert!(!serialized.contains("approvals_reviewer")); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 48bde3f177..6df989db16 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -98,12 +98,12 @@ use std::path::Path; use std::path::PathBuf; use crate::config::permissions::compile_permission_profile; +use crate::config::permissions::get_readable_roots_required_for_codex_runtime; use crate::config::permissions::network_proxy_config_from_profile_network; use crate::config::profile::ConfigProfile; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; use toml_edit::DocumentMut; -use toml_edit::value; pub(crate) mod agent_roles; pub mod edit; @@ -651,9 +651,6 @@ impl ConfigBuilder { fallback_cwd, } = self; let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?; - if let Err(err) = maybe_migrate_smart_approvals_alias(&codex_home).await { - tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias"); - } let cli_overrides = cli_overrides.unwrap_or_default(); let mut harness_overrides = harness_overrides.unwrap_or_default(); let loader_overrides = loader_overrides.unwrap_or_default(); @@ -701,111 +698,6 @@ impl ConfigBuilder { } } -fn config_scope_segments(scope: &[String], key: &str) -> Vec { - let mut segments = scope.to_vec(); - segments.push(key.to_string()); - segments -} - -fn feature_scope_segments(scope: &[String], feature_key: &str) -> Vec { - let mut segments = scope.to_vec(); - segments.push("features".to_string()); - segments.push(feature_key.to_string()); - segments -} - -fn push_smart_approvals_alias_migration_edits( - edits: &mut Vec, - scope: &[String], - features: &FeaturesToml, - approvals_reviewer_missing: bool, -) { - let Some(alias_enabled) = features.entries.get("smart_approvals").copied() else { - return; - }; - let canonical_enabled = features - .entries - .get("guardian_approval") - .copied() - .unwrap_or(alias_enabled); - - if !features.entries.contains_key("guardian_approval") { - edits.push(ConfigEdit::SetPath { - segments: feature_scope_segments(scope, "guardian_approval"), - value: value(alias_enabled), - }); - } - if canonical_enabled && approvals_reviewer_missing { - edits.push(ConfigEdit::SetPath { - segments: config_scope_segments(scope, "approvals_reviewer"), - value: value(ApprovalsReviewer::GuardianSubagent.to_string()), - }); - } - edits.push(ConfigEdit::ClearPath { - segments: feature_scope_segments(scope, "smart_approvals"), - }); -} - -/// Rewrites the legacy `smart_approvals` feature flag to -/// `guardian_approval` in `config.toml` before normal config loading. -/// -/// If the old key is present, this preserves its value by setting -/// `guardian_approval = ` when the new key is not already present. -/// Because the deprecated flag historically meant "turn guardian review on", -/// this migration also backfills `approvals_reviewer = "guardian_subagent"` -/// in the same scope when that reviewer is not already configured there and the -/// migrated feature value is `true`. -/// In all cases it removes the deprecated `smart_approvals` entry so future -/// loads only see the canonical feature flag name. -async fn maybe_migrate_smart_approvals_alias(codex_home: &Path) -> std::io::Result { - let config_path = codex_home.join(CONFIG_TOML_FILE); - if !tokio::fs::try_exists(&config_path).await? { - return Ok(false); - } - - let config_contents = tokio::fs::read_to_string(&config_path).await?; - let Ok(config_toml) = toml::from_str::(&config_contents) else { - return Ok(false); - }; - - let mut edits = Vec::new(); - - let root_scope = Vec::new(); - if let Some(features) = config_toml.features.as_ref() { - push_smart_approvals_alias_migration_edits( - &mut edits, - &root_scope, - features, - config_toml.approvals_reviewer.is_none(), - ); - } - - for (profile_name, profile) in &config_toml.profiles { - if let Some(features) = profile.features.as_ref() { - let scope = vec!["profiles".to_string(), profile_name.clone()]; - push_smart_approvals_alias_migration_edits( - &mut edits, - &scope, - features, - profile.approvals_reviewer.is_none(), - ); - } - } - - if edits.is_empty() { - return Ok(false); - } - - ConfigEditsBuilder::new(codex_home) - .with_edits(edits) - .apply() - .await - .map_err(|err| { - std::io::Error::other(format!("failed to migrate guardian_approval alias: {err}")) - })?; - Ok(true) -} - impl Config { /// This is the preferred way to create an instance of [Config]. pub async fn load_with_cli_overrides( @@ -867,9 +759,6 @@ pub async fn load_config_as_toml_with_cli_overrides( cwd: &AbsolutePathBuf, cli_overrides: Vec<(String, TomlValue)>, ) -> std::io::Result { - if let Err(err) = maybe_migrate_smart_approvals_alias(codex_home).await { - tracing::warn!(error = %err, "failed to migrate smart_approvals feature alias"); - } let config_layer_stack = load_config_layers_state( codex_home, Some(cwd.clone()), @@ -1918,29 +1807,6 @@ fn resolve_permission_config_syntax( }) } -fn add_additional_file_system_writes( - file_system_sandbox_policy: &mut FileSystemSandboxPolicy, - additional_writable_roots: &[AbsolutePathBuf], -) { - for path in additional_writable_roots { - let exists = file_system_sandbox_policy.entries.iter().any(|entry| { - matches!( - &entry.path, - codex_protocol::permissions::FileSystemPath::Path { path: existing } - if existing == path && entry.access == codex_protocol::permissions::FileSystemAccessMode::Write - ) - }); - if !exists { - file_system_sandbox_policy.entries.push( - codex_protocol::permissions::FileSystemSandboxEntry { - path: codex_protocol::permissions::FileSystemPath::Path { path: path.clone() }, - access: codex_protocol::permissions::FileSystemAccessMode::Write, - }, - ); - } - } -} - /// Optional overrides for user configuration (e.g., from CLI flags). #[derive(Default, Debug, Clone)] pub struct ConfigOverrides { @@ -2309,10 +2175,8 @@ impl Config { let mut sandbox_policy = file_system_sandbox_policy .to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?; if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { - add_additional_file_system_writes( - &mut file_system_sandbox_policy, - &additional_writable_roots, - ); + file_system_sandbox_policy = file_system_sandbox_policy + .with_additional_writable_roots(&resolved_cwd, &additional_writable_roots); sandbox_policy = file_system_sandbox_policy .to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?; } @@ -2663,6 +2527,11 @@ impl Config { } else { network.enabled().then_some(network) }; + let helper_readable_roots = get_readable_roots_required_for_codex_runtime( + &codex_home, + zsh_path.as_ref(), + main_execve_wrapper_exe.as_ref(), + ); let effective_sandbox_policy = constrained_sandbox_policy.value.get().clone(); let effective_file_system_sandbox_policy = if effective_sandbox_policy == original_sandbox_policy { @@ -2673,6 +2542,8 @@ impl Config { &resolved_cwd, ) }; + let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy + .with_additional_readable_roots(&resolved_cwd, &helper_readable_roots); let effective_network_sandbox_policy = if effective_sandbox_policy == original_sandbox_policy { network_sandbox_policy diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 4d1027efe0..759c269b76 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -190,6 +190,37 @@ pub(crate) fn compile_permission_profile( )) } +/// Returns a list of paths that must be readable by shell tools in order +/// for Codex to function. These should always be added to the +/// `FileSystemSandboxPolicy` for a thread. +pub(crate) fn get_readable_roots_required_for_codex_runtime( + codex_home: &Path, + zsh_path: Option<&PathBuf>, + main_execve_wrapper_exe: Option<&PathBuf>, +) -> Vec { + let arg0_root = AbsolutePathBuf::from_absolute_path(codex_home.join("tmp").join("arg0")).ok(); + let zsh_path = zsh_path.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()); + let execve_wrapper_root = main_execve_wrapper_exe.and_then(|path| { + let path = AbsolutePathBuf::from_absolute_path(path).ok()?; + if let Some(arg0_root) = arg0_root.as_ref() + && path.as_path().starts_with(arg0_root.as_path()) + { + path.parent() + } else { + Some(path) + } + }); + + let mut readable_roots = Vec::new(); + if let Some(zsh_path) = zsh_path { + readable_roots.push(zsh_path); + } + if let Some(execve_wrapper_root) = execve_wrapper_root { + readable_roots.push(execve_wrapper_root); + } + readable_roots +} + fn compile_network_sandbox_policy(network: Option<&NetworkToml>) -> NetworkSandboxPolicy { let Some(network) = network else { return NetworkSandboxPolicy::Restricted; diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 036c8450cd..b90b903e88 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -1,5 +1,11 @@ use super::*; +use crate::config::Config; +use crate::config::ConfigOverrides; +use crate::config::ConfigToml; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; +use std::collections::BTreeMap; +use tempfile::TempDir; #[test] fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() { @@ -7,3 +13,66 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() { normalize_absolute_path_for_platform(r"\\?\D:\c\x\worktrees\2508\swift-base", true); assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base")); } + +#[test] +fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let cwd = temp_dir.path().join("workspace"); + let codex_home = temp_dir.path().join(".codex"); + let zsh_path = temp_dir.path().join("runtime").join("zsh"); + let arg0_root = codex_home.join("tmp").join("arg0"); + let allowed_arg0_dir = arg0_root.join("codex-arg0-session"); + let sibling_arg0_dir = arg0_root.join("codex-arg0-other-session"); + let execve_wrapper = allowed_arg0_dir.join("codex-execve-wrapper"); + std::fs::create_dir_all(&cwd)?; + std::fs::create_dir_all(zsh_path.parent().expect("zsh path should have parent"))?; + std::fs::create_dir_all(&allowed_arg0_dir)?; + std::fs::create_dir_all(&sibling_arg0_dir)?; + std::fs::write(&zsh_path, "")?; + std::fs::write(&execve_wrapper, "")?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some("workspace".to_string()), + permissions: Some(PermissionsToml { + entries: BTreeMap::from([( + "workspace".to_string(), + PermissionProfileToml { + filesystem: Some(FilesystemPermissionsToml { + entries: BTreeMap::new(), + }), + network: None, + }, + )]), + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.clone()), + zsh_path: Some(zsh_path.clone()), + main_execve_wrapper_exe: Some(execve_wrapper), + ..Default::default() + }, + codex_home, + )?; + + let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?; + let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?; + let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_arg0_dir)?; + let policy = &config.permissions.file_system_sandbox_policy; + + assert!( + policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd), + "expected zsh helper path to be readable, policy: {policy:?}" + ); + assert!( + policy.can_read_path_with_cwd(expected_allowed_arg0_dir.as_path(), &cwd), + "expected active arg0 helper dir to be readable, policy: {policy:?}" + ); + assert!( + !policy.can_read_path_with_cwd(expected_sibling_arg0_dir.as_path(), &cwd), + "expected sibling arg0 helper dir to remain unreadable, policy: {policy:?}" + ); + + Ok(()) +} diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 871cf502aa..c122187901 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -43,6 +43,7 @@ fn build_permissions_update_item( Some(DeveloperInstructions::from_policy( next.sandbox_policy.get(), next.approval_policy.value(), + next.config.approvals_reviewer, exec_policy, &next.cwd, next.features.enabled(Feature::ExecPermissionApprovals), diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 0c95af4c02..a027f2461b 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -549,7 +549,7 @@ pub fn render_decision_for_unmatched_command( // On Windows, ReadOnly sandbox is not a real sandbox, so special-case it // here. - let runtime_sandbox_provides_safety = + let environment_lacks_sandbox_protections = cfg!(windows) && matches!(sandbox_policy, SandboxPolicy::ReadOnly { .. }); // If the command is flagged as dangerous or we have no sandbox protection, @@ -558,9 +558,20 @@ pub fn render_decision_for_unmatched_command( // We prefer to prompt the user rather than outright forbid the command, // but if the user has explicitly disabled prompts, we must // forbid the command. - if command_might_be_dangerous(command) || runtime_sandbox_provides_safety { + if command_might_be_dangerous(command) || environment_lacks_sandbox_protections { return match approval_policy { - AskForApproval::Never => Decision::Forbidden, + AskForApproval::Never => { + let sandbox_is_explicitly_disabled = matches!( + sandbox_policy, + SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + ); + if sandbox_is_explicitly_disabled { + // If the sandbox is explicitly disabled, we should allow the command to run + Decision::Allow + } else { + Decision::Forbidden + } + } AskForApproval::OnFailure | AskForApproval::OnRequest | AskForApproval::UnlessTrusted diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index fd3fe05e11..f062bcc741 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -81,6 +81,10 @@ fn unrestricted_file_system_sandbox_policy() -> FileSystemSandboxPolicy { FileSystemSandboxPolicy::unrestricted() } +fn external_file_system_sandbox_policy() -> FileSystemSandboxPolicy { + FileSystemSandboxPolicy::external_sandbox() +} + async fn test_config() -> (TempDir, Config) { let home = TempDir::new().expect("create temp dir"); let config = ConfigBuilder::default() @@ -503,39 +507,25 @@ async fn loads_policies_from_multiple_config_layers() -> anyhow::Result<()> { #[tokio::test] async fn evaluates_bash_lc_inner_commands() { - let policy_src = r#" -prefix_rule(pattern=["rm"], decision="forbidden") -"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - - let forbidden_script = vec![ - "bash".to_string(), - "-lc".to_string(), - "rm -rf /some/important/folder".to_string(), - ]; - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &forbidden_script, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["rm"], decision="forbidden")"#.to_string()), + command: vec![ + "bash".to_string(), + "-lc".to_string(), + "rm -rf /some/important/folder".to_string(), + ], approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, - ExecApprovalRequirement::Forbidden { - reason: "`bash -lc 'rm -rf /some/important/folder'` rejected: policy forbids commands starting with `rm`".to_string() - } - ); + }, + ExecApprovalRequirement::Forbidden { + reason: "`bash -lc 'rm -rf /some/important/folder'` rejected: policy forbids commands starting with `rm`".to_string(), + }, + ) + .await; } #[test] @@ -558,163 +548,132 @@ fn commands_for_exec_policy_falls_back_for_whitespace_shell_script() { #[tokio::test] async fn evaluates_heredoc_script_against_prefix_rules() { - let policy_src = r#"prefix_rule(pattern=["python3"], decision="allow")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); let command = vec![ "bash".to_string(), "-lc".to_string(), "python3 <<'PY'\nprint('hello')\nPY".to_string(), ]; - let requirement = ExecPolicyManager::new(policy) - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["python3"], decision="allow")"#.to_string()), + command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Skip { bypass_sandbox: true, proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] async fn omits_auto_amendment_for_heredoc_fallback_prompts() { - let command = vec![ - "bash".to_string(), - "-lc".to_string(), - "python3 <<'PY'\nprint('hello')\nPY".to_string(), - ]; - - let requirement = ExecPolicyManager::default() - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: vec![ + "bash".to_string(), + "-lc".to_string(), + "python3 <<'PY'\nprint('hello')\nPY".to_string(), + ], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] async fn drops_requested_amendment_for_heredoc_fallback_prompts_when_it_wont_match() { - let command = vec![ - "bash".to_string(), - "-lc".to_string(), - "python3 <<'PY'\nprint('hello')\nPY".to_string(), - ]; - let requested_prefix = vec!["python3".to_string(), "-m".to_string(), "pip".to_string()]; - - let requirement = ExecPolicyManager::default() - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: vec![ + "bash".to_string(), + "-lc".to_string(), + "python3 <<'PY'\nprint('hello')\nPY".to_string(), + ], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, - prefix_rule: Some(requested_prefix.clone()), - }) - .await; - - assert_eq!( - requirement, + prefix_rule: Some(vec![ + "python3".to_string(), + "-m".to_string(), + "pip".to_string(), + ]), + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] async fn justification_is_included_in_forbidden_exec_approval_requirement() { - let policy_src = r#" + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some( + r#" prefix_rule( pattern=["rm"], decision="forbidden", justification="destructive command", ) -"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &[ +"# + .to_string(), + ), + command: vec![ "rm".to_string(), "-rf".to_string(), "/some/important/folder".to_string(), ], approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Forbidden { - reason: "`rm -rf /some/important/folder` rejected: destructive command".to_string() - } - ); + reason: "`rm -rf /some/important/folder` rejected: destructive command".to_string(), + }, + ) + .await; } #[tokio::test] async fn exec_approval_requirement_prefers_execpolicy_match() { - let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - let command = vec!["rm".to_string()]; - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["rm"], decision="prompt")"#.to_string()), + command: vec!["rm".to_string()], approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: Some("`rm` requires approval by policy".to_string()), proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] @@ -727,31 +686,22 @@ host_executable(name = "git", paths = ["{git_path_literal}"]) prefix_rule(pattern=["git"], decision="allow") "# ); - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", &policy_src) - .expect("parse policy"); - let manager = ExecPolicyManager::new(Arc::new(parser.build())); - let command = vec![git_path, "status".to_string()]; - - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(policy_src), + command: vec![git_path, "status".to_string()], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Skip { bypass_sandbox: true, proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] @@ -770,93 +720,71 @@ host_executable(name = "git", paths = ["{allowed_git_path_literal}"]) prefix_rule(pattern=["git"], decision="prompt") "# ); - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", &policy_src) - .expect("parse policy"); - let manager = ExecPolicyManager::new(Arc::new(parser.build())); - let command = vec![disallowed_git_path, "status".to_string()]; - - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(policy_src), + command: vec![disallowed_git_path.clone(), "status".to_string()], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Skip { bypass_sandbox: false, - proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), - } - ); + proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ + disallowed_git_path, + "status".to_string(), + ])), + }, + ) + .await; } #[tokio::test] async fn requested_prefix_rule_can_approve_absolute_path_commands() { - let command = vec![ - host_program_path("cargo"), - "install".to_string(), - "cargo-insta".to_string(), - ]; - let manager = ExecPolicyManager::default(); - - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: vec![ + host_program_path("cargo"), + "install".to_string(), + "cargo-insta".to_string(), + ], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]), - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ "cargo".to_string(), "install".to_string(), ])), - } - ); + }, + ) + .await; } #[tokio::test] async fn exec_approval_requirement_respects_approval_policy() { - let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - let command = vec!["rm".to_string()]; - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["rm"], decision="prompt")"#.to_string()), + command: vec!["rm".to_string()], approval_policy: AskForApproval::Never, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Forbidden { - reason: PROMPT_CONFLICT_REASON.to_string() - } - ); + reason: PROMPT_CONFLICT_REASON.to_string(), + }, + ) + .await; } #[test] @@ -903,11 +831,10 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() { #[tokio::test] async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_granular_sandbox_is_disabled() { - let command = vec!["madeup-cmd".to_string()]; - - let requirement = ExecPolicyManager::default() - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: vec!["madeup-cmd".to_string()], approval_policy: AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: false, rules: true, @@ -915,19 +842,16 @@ async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_gra request_permissions: true, mcp_elicitations: true, }), - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Forbidden { reason: REJECT_SANDBOX_APPROVAL_REASON.to_string(), - } - ); + }, + ) + .await; } #[tokio::test] @@ -1239,178 +1163,140 @@ async fn append_execpolicy_amendment_rejects_empty_prefix() { async fn proposed_execpolicy_amendment_is_present_for_single_command_without_policy_match() { let command = vec!["cargo".to_string(), "build".to_string()]; - let manager = ExecPolicyManager::default(); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: command.clone(), approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: None, - proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)) - } - ); + proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), + }, + ) + .await; } #[tokio::test] async fn proposed_execpolicy_amendment_is_omitted_when_policy_prompts() { - let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - let command = vec!["rm".to_string()]; - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["rm"], decision="prompt")"#.to_string()), + command: vec!["rm".to_string()], approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: Some("`rm` requires approval by policy".to_string()), proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } #[tokio::test] async fn proposed_execpolicy_amendment_is_present_for_multi_command_scripts() { - let command = vec![ - "bash".to_string(), - "-lc".to_string(), - "cargo build && echo ok".to_string(), - ]; - let manager = ExecPolicyManager::default(); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: vec![ + "bash".to_string(), + "-lc".to_string(), + "cargo build && echo ok".to_string(), + ], approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ "cargo".to_string(), - "build".to_string() + "build".to_string(), ])), - } - ); + }, + ) + .await; } #[tokio::test] async fn proposed_execpolicy_amendment_uses_first_no_match_in_multi_command_scripts() { let policy_src = r#"prefix_rule(pattern=["cat"], decision="allow")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - let command = vec![ "bash".to_string(), "-lc".to_string(), "cat && apple".to_string(), ]; - assert_eq!( - ExecPolicyManager::new(policy) - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, - approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), - sandbox_permissions: SandboxPermissions::UseDefault, - prefix_rule: None, - }) - .await, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(policy_src.to_string()), + command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ - "apple".to_string() + "apple".to_string(), ])), - } - ); + }, + ) + .await; } #[tokio::test] async fn proposed_execpolicy_amendment_is_present_when_heuristics_allow() { let command = vec!["echo".to_string(), "safe".to_string()]; - let manager = ExecPolicyManager::default(); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: command.clone(), approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Skip { bypass_sandbox: false, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), - } - ); + }, + ) + .await; } #[tokio::test] async fn proposed_execpolicy_amendment_is_suppressed_when_policy_matches_allow() { - let policy_src = r#"prefix_rule(pattern=["echo"], decision="allow")"#; - let mut parser = PolicyParser::new(); - parser - .parse("test.rules", policy_src) - .expect("parse policy"); - let policy = Arc::new(parser.build()); - let command = vec!["echo".to_string(), "safe".to_string()]; - - let manager = ExecPolicyManager::new(policy); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(r#"prefix_rule(pattern=["echo"], decision="allow")"#.to_string()), + command: vec!["echo".to_string(), "safe".to_string()], approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), - file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::Skip { bypass_sandbox: true, proposed_execpolicy_amendment: None, - } - ); + }, + ) + .await; } fn derive_requested_execpolicy_amendment_for_test( @@ -1560,25 +1446,23 @@ fn derive_requested_execpolicy_amendment_returns_none_when_policy_matches() { #[tokio::test] async fn dangerous_rm_rf_requires_approval_in_danger_full_access() { let command = vec_str(&["rm", "-rf", "/tmp/nonexistent"]); - let manager = ExecPolicyManager::default(); - let requirement = manager - .create_exec_approval_requirement_for_command(ExecApprovalRequest { - command: &command, + + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command: command.clone(), approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, - }) - .await; - - assert_eq!( - requirement, + }, ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), - } - ); + }, + ) + .await; } fn vec_str(items: &[&str]) -> Vec { @@ -1686,3 +1570,100 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() { (unless AskForApproval::Never is specified)."# ); } + +#[tokio::test] +async fn dangerous_command_allowed_when_sandbox_is_explicitly_disabled() { + let command = vec_str(&["rm", "-rf", "/tmp/nonexistent"]); + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: None, + command, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::ExternalSandbox { + network_access: Default::default(), + }, + file_system_sandbox_policy: external_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }, + ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: Some(ExecPolicyAmendment { + command: vec_str(&["rm", "-rf", "/tmp/nonexistent"]), + }), + }, + ) + .await; +} + +#[tokio::test] +async fn dangerous_command_forbidden_in_external_sandbox_when_policy_matches() { + let command = vec_str(&["rm", "-rf", "/tmp/nonexistent"]); + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some("prefix_rule(pattern=['rm'], decision='prompt')".to_string()), + command, + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::ExternalSandbox { + network_access: Default::default(), + }, + file_system_sandbox_policy: external_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }, + ExecApprovalRequirement::Forbidden { + reason: "approval required by policy, but AskForApproval is set to Never".to_string(), + }, + ) + .await; +} + +struct ExecApprovalRequirementScenario { + /// Source for the Starlark `.rules` file. + policy_src: Option, + command: Vec, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + file_system_sandbox_policy: FileSystemSandboxPolicy, + sandbox_permissions: SandboxPermissions, + prefix_rule: Option>, +} + +async fn assert_exec_approval_requirement_for_command( + test: ExecApprovalRequirementScenario, + expected_requirement: ExecApprovalRequirement, +) { + let ExecApprovalRequirementScenario { + policy_src, + command, + approval_policy, + sandbox_policy, + file_system_sandbox_policy, + sandbox_permissions, + prefix_rule, + } = test; + + let policy = match policy_src { + Some(src) => { + let mut parser = PolicyParser::new(); + parser + .parse("test.rules", src.as_str()) + .expect("parse policy"); + Arc::new(parser.build()) + } + None => Arc::new(Policy::empty()), + }; + + let requirement = ExecPolicyManager::new(policy) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + sandbox_permissions, + prefix_rule, + }) + .await; + + assert_eq!(requirement, expected_requirement); +} diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 3e7f0cb84f..f82cc73bb5 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -457,6 +457,9 @@ const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title"; const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description"; const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params"; const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display"; +const MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_DEFAULT: &str = "mcp_tool_call__default"; +const MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_ALWAYS_ALLOW: &str = "mcp_tool_call__always_allow"; +const MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_FULL_ACCESS: &str = "mcp_tool_call__full_access"; pub(crate) fn is_mcp_tool_approval_question_id(question_id: &str) -> bool { question_id @@ -494,14 +497,22 @@ async fn maybe_request_mcp_tool_approval( let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref()); let approval_required = annotations.is_some_and(requires_mcp_tool_approval); let mut monitor_reason = None; + let auto_approved_by_policy = approval_mode == AppToolApproval::Approve + || (approval_mode == AppToolApproval::Auto && is_full_access_mode(turn_context)); - if approval_mode == AppToolApproval::Approve { + if auto_approved_by_policy { if !approval_required { return None; } - match maybe_monitor_auto_approved_mcp_tool_call(sess, turn_context, invocation, metadata) - .await + match maybe_monitor_auto_approved_mcp_tool_call( + sess, + turn_context, + invocation, + metadata, + approval_mode, + ) + .await { ArcMonitorOutcome::Ok => return None, ArcMonitorOutcome::AskUser(reason) => { @@ -515,13 +526,8 @@ async fn maybe_request_mcp_tool_approval( } } - if approval_mode == AppToolApproval::Auto { - if is_full_access_mode(turn_context) { - return None; - } - if !approval_required { - return None; - } + if approval_mode == AppToolApproval::Auto && !approval_required { + return None; } let session_approval_key = session_mcp_tool_approval_key(invocation, metadata, approval_mode); @@ -653,9 +659,16 @@ async fn maybe_monitor_auto_approved_mcp_tool_call( turn_context: &TurnContext, invocation: &McpInvocation, metadata: Option<&McpToolApprovalMetadata>, + approval_mode: AppToolApproval, ) -> ArcMonitorOutcome { let action = prepare_arc_request_action(invocation, metadata); - monitor_action(sess, turn_context, action).await + monitor_action( + sess, + turn_context, + action, + mcp_tool_approval_callsite_mode(approval_mode, turn_context), + ) + .await } fn prepare_arc_request_action( @@ -749,6 +762,22 @@ fn is_full_access_mode(turn_context: &TurnContext) -> bool { ) } +fn mcp_tool_approval_callsite_mode( + approval_mode: AppToolApproval, + turn_context: &TurnContext, +) -> &'static str { + match approval_mode { + AppToolApproval::Approve => MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_ALWAYS_ALLOW, + AppToolApproval::Auto | AppToolApproval::Prompt => { + if approval_mode == AppToolApproval::Auto && is_full_access_mode(turn_context) { + MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_FULL_ACCESS + } else { + MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_DEFAULT + } + } + } +} + pub(crate) async fn lookup_mcp_tool_metadata( sess: &Session, turn_context: &TurnContext, diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 5537e680ed..0ac37f9ce8 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -776,6 +776,38 @@ fn approval_elicitation_meta_merges_session_and_always_persist_with_connector_so ); } +#[tokio::test] +async fn approval_callsite_mode_distinguishes_default_always_allow_and_full_access() { + let (_session, mut turn_context) = make_session_and_context().await; + + assert_eq!( + mcp_tool_approval_callsite_mode(AppToolApproval::Auto, &turn_context), + "mcp_tool_call__default" + ); + assert_eq!( + mcp_tool_approval_callsite_mode(AppToolApproval::Prompt, &turn_context), + "mcp_tool_call__default" + ); + assert_eq!( + mcp_tool_approval_callsite_mode(AppToolApproval::Approve, &turn_context), + "mcp_tool_call__always_allow" + ); + + turn_context + .approval_policy + .set(AskForApproval::Never) + .expect("test setup should allow updating approval policy"); + turn_context + .sandbox_policy + .set(SandboxPolicy::DangerFullAccess) + .expect("test setup should allow updating sandbox policy"); + + assert_eq!( + mcp_tool_approval_callsite_mode(AppToolApproval::Auto, &turn_context), + "mcp_tool_call__full_access" + ); +} + #[test] fn declined_elicitation_response_stays_decline() { let response = parse_mcp_tool_approval_elicitation_response( @@ -1035,6 +1067,83 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() { ); } +#[tokio::test] +async fn full_access_auto_mode_blocks_when_arc_returns_interrupt_for_model() { + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/codex/safety/arc")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "outcome": "steer-model", + "short_reason": "needs approval", + "rationale": "high-risk action", + "risk_score": 96, + "risk_level": "critical", + "evidence": [{ + "message": "dangerous_tool", + "why": "high-risk action", + }], + }))) + .expect(1) + .mount(&server) + .await; + + let (session, mut turn_context) = make_session_and_context().await; + turn_context.auth_manager = Some(crate::test_support::auth_manager_from_auth( + crate::CodexAuth::create_dummy_chatgpt_auth_for_testing(), + )); + turn_context + .approval_policy + .set(AskForApproval::Never) + .expect("test setup should allow updating approval policy"); + turn_context + .sandbox_policy + .set(SandboxPolicy::DangerFullAccess) + .expect("test setup should allow updating sandbox policy"); + let mut config = (*turn_context.config).clone(); + config.chatgpt_base_url = server.uri(); + turn_context.config = Arc::new(config); + + let session = Arc::new(session); + let turn_context = Arc::new(turn_context); + let invocation = McpInvocation { + server: CODEX_APPS_MCP_SERVER_NAME.to_string(), + tool: "dangerous_tool".to_string(), + arguments: Some(serde_json::json!({ "id": 1 })), + }; + let metadata = McpToolApprovalMetadata { + annotations: Some(annotations(Some(false), Some(true), Some(true))), + connector_id: Some("calendar".to_string()), + connector_name: Some("Calendar".to_string()), + connector_description: Some("Manage events".to_string()), + tool_title: Some("Dangerous Tool".to_string()), + tool_description: Some("Performs a risky action.".to_string()), + codex_apps_meta: None, + }; + + let decision = maybe_request_mcp_tool_approval( + &session, + &turn_context, + "call-2", + &invocation, + Some(&metadata), + AppToolApproval::Auto, + ) + .await; + + assert_eq!( + decision, + Some(McpToolApprovalDecision::BlockedBySafetyMonitor( + "Tool call was cancelled because of safety risks: high-risk action".to_string(), + )) + ); +} + #[tokio::test] async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_enabled() { use wiremock::Mock; diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index a4473a30dc..05899799f7 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -626,7 +626,8 @@ impl PluginsManager { if let Some(featured_plugin_ids) = self.cached_featured_plugin_ids(&cache_key) { return Ok(featured_plugin_ids); } - let featured_plugin_ids = fetch_remote_featured_plugin_ids(config, auth).await?; + let featured_plugin_ids = + fetch_remote_featured_plugin_ids(config, auth, self.restriction_product).await?; self.write_featured_plugin_ids_cache(cache_key, &featured_plugin_ids); Ok(featured_plugin_ids) } diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index 52460fcc25..b09ddfc78d 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -23,6 +23,7 @@ use wiremock::ResponseTemplate; use wiremock::matchers::header; use wiremock::matchers::method; use wiremock::matchers::path; +use wiremock::matchers::query_param; fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) { let plugin_root = root.join(dir_name); @@ -1949,6 +1950,74 @@ plugins = true ); } +#[tokio::test] +async fn featured_plugin_ids_for_config_uses_restriction_product_query_param() { + let tmp = tempfile::tempdir().unwrap(); + write_file( + &tmp.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +"#, + ); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "chat")) + .and(header("authorization", "Bearer Access Token")) + .and(header("chatgpt-account-id", "account_id")) + .respond_with(ResponseTemplate::new(200).set_body_string(r#"["chat-plugin"]"#)) + .mount(&server) + .await; + + let mut config = load_config(tmp.path(), tmp.path()).await; + config.chatgpt_base_url = format!("{}/backend-api/", server.uri()); + let manager = PluginsManager::new_with_restriction_product( + tmp.path().to_path_buf(), + Some(Product::Chatgpt), + ); + + let featured_plugin_ids = manager + .featured_plugin_ids_for_config( + &config, + Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()), + ) + .await + .unwrap(); + + assert_eq!(featured_plugin_ids, vec!["chat-plugin".to_string()]); +} + +#[tokio::test] +async fn featured_plugin_ids_for_config_defaults_query_param_to_codex() { + let tmp = tempfile::tempdir().unwrap(); + write_file( + &tmp.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +"#, + ); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) + .respond_with(ResponseTemplate::new(200).set_body_string(r#"["codex-plugin"]"#)) + .mount(&server) + .await; + + let mut config = load_config(tmp.path(), tmp.path()).await; + config.chatgpt_base_url = format!("{}/backend-api/", server.uri()); + let manager = PluginsManager::new_with_restriction_product(tmp.path().to_path_buf(), None); + + let featured_plugin_ids = manager + .featured_plugin_ids_for_config(&config, None) + .await + .unwrap(); + + assert_eq!(featured_plugin_ids, vec!["codex-plugin".to_string()]); +} + #[test] fn refresh_curated_plugin_cache_replaces_existing_local_version_with_sha() { let tmp = tempfile::tempdir().unwrap(); diff --git a/codex-rs/core/src/plugins/remote.rs b/codex-rs/core/src/plugins/remote.rs index 898767e35f..756824b339 100644 --- a/codex-rs/core/src/plugins/remote.rs +++ b/codex-rs/core/src/plugins/remote.rs @@ -1,6 +1,7 @@ use crate::auth::CodexAuth; use crate::config::Config; use crate::default_client::build_reqwest_client; +use codex_protocol::protocol::Product; use serde::Deserialize; use std::time::Duration; use url::Url; @@ -162,12 +163,17 @@ pub(crate) async fn fetch_remote_plugin_status( pub async fn fetch_remote_featured_plugin_ids( config: &Config, auth: Option<&CodexAuth>, + product: Option, ) -> Result, RemotePluginFetchError> { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/featured"); let client = build_reqwest_client(); let mut request = client .get(&url) + .query(&[( + "platform", + product.unwrap_or(Product::Codex).to_app_platform(), + )]) .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT); if let Some(auth) = auth.filter(|auth| auth.is_chatgpt_auth()) { diff --git a/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap b/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap new file mode 100644 index 0000000000..90bb82d409 --- /dev/null +++ b/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap @@ -0,0 +1,17 @@ +--- +source: core/src/codex_tests.rs +assertion_line: 1282 +expression: snapshot +--- +Scenario: First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode. + +## First Forked Turn Request +00:message/developer: +01:message/user:> +02:message/user:fork seed +03:message/developer: +04:message/user:> +05:message/developer[2]: + [01] + [02] Fork turn collaboration instructions. +06:message/user:after fork diff --git a/codex-rs/core/src/tools/code_mode/bridge.js b/codex-rs/core/src/tools/code_mode/bridge.js deleted file mode 100644 index 0c61a9db19..0000000000 --- a/codex-rs/core/src/tools/code_mode/bridge.js +++ /dev/null @@ -1,51 +0,0 @@ -const __codexContentItems = Array.isArray(globalThis.__codexContentItems) - ? globalThis.__codexContentItems - : []; -const __codexRuntime = globalThis.__codexRuntime; - -delete globalThis.__codexRuntime; - -Object.defineProperty(globalThis, '__codexContentItems', { - value: __codexContentItems, - configurable: true, - enumerable: false, - writable: false, -}); - -(() => { - if (!__codexRuntime || typeof __codexRuntime !== 'object') { - throw new Error('code mode runtime is unavailable'); - } - - function defineGlobal(name, value) { - Object.defineProperty(globalThis, name, { - value, - configurable: true, - enumerable: true, - writable: false, - }); - } - - defineGlobal('ALL_TOOLS', __codexRuntime.ALL_TOOLS); - defineGlobal('exit', __codexRuntime.exit); - defineGlobal('image', __codexRuntime.image); - defineGlobal('load', __codexRuntime.load); - defineGlobal('notify', __codexRuntime.notify); - defineGlobal('store', __codexRuntime.store); - defineGlobal('text', __codexRuntime.text); - defineGlobal('tools', __codexRuntime.tools); - defineGlobal('yield_control', __codexRuntime.yield_control); - - defineGlobal( - 'console', - Object.freeze({ - log() {}, - info() {}, - warn() {}, - error() {}, - debug() {}, - }) - ); -})(); - -__CODE_MODE_USER_CODE_PLACEHOLDER__ diff --git a/codex-rs/core/src/tools/code_mode/description.md b/codex-rs/core/src/tools/code_mode/description.md deleted file mode 100644 index e0a124c65f..0000000000 --- a/codex-rs/core/src/tools/code_mode/description.md +++ /dev/null @@ -1,19 +0,0 @@ -## exec -- Runs raw JavaScript in an isolated context (no Node, no file system, or network access, no console). -- Send raw JavaScript source text, not JSON, quoted strings, or markdown code fences. -- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. -- `yield_time_ms` asks `exec` to yield early after that many milliseconds if the script is still running. -- `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. -- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`. -- Tool methods take either string or object as parameter. -- They return either a structured value or a string based on the description above. - -- Global helpers: -- `exit()`: Immediately ends the current script successfully (like an early return from the top level). -- `text(value: string | number | boolean | undefined | null)`: Appends a text item and returns it. Non-string values are stringified with `JSON.stringify(...)` when possible. -- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null })`: Appends an image item and returns it. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. -- `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. -- `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. -- `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`. -- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries. -- `yield_control()`: yields the accumulated output to the model immediately while the script keeps running. diff --git a/codex-rs/core/src/tools/code_mode/execute_handler.rs b/codex-rs/core/src/tools/code_mode/execute_handler.rs index 9eba126dd1..3f77216c16 100644 --- a/codex-rs/core/src/tools/code_mode/execute_handler.rs +++ b/codex-rs/core/src/tools/code_mode/execute_handler.rs @@ -1,8 +1,5 @@ use async_trait::async_trait; -use serde::Deserialize; -use crate::codex::Session; -use crate::codex::TurnContext; use crate::function_tool::FunctionCallError; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; @@ -10,180 +7,52 @@ use crate::tools::context::ToolPayload; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; -use super::CODE_MODE_PRAGMA_PREFIX; -use super::CodeModeSessionProgress; use super::ExecContext; use super::PUBLIC_TOOL_NAME; use super::build_enabled_tools; -use super::handle_node_message; -use super::protocol::HostToNodeMessage; -use super::protocol::build_source; +use super::handle_runtime_response; pub struct CodeModeExecuteHandler; -const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1; - -#[derive(Debug, Default, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -struct CodeModeExecPragma { - #[serde(default)] - yield_time_ms: Option, - #[serde(default)] - max_output_tokens: Option, -} - -#[derive(Debug, PartialEq, Eq)] -struct CodeModeExecArgs { - code: String, - yield_time_ms: Option, - max_output_tokens: Option, -} impl CodeModeExecuteHandler { async fn execute( &self, - session: std::sync::Arc, - turn: std::sync::Arc, + session: std::sync::Arc, + turn: std::sync::Arc, call_id: String, code: String, ) -> Result { - let args = parse_freeform_args(&code)?; + let args = + codex_code_mode::parse_exec_source(&code).map_err(FunctionCallError::RespondToModel)?; let exec = ExecContext { session, turn }; let enabled_tools = build_enabled_tools(&exec).await; - let service = &exec.session.services.code_mode_service; - let stored_values = service.stored_values().await; - let source = - build_source(&args.code, &enabled_tools).map_err(FunctionCallError::RespondToModel)?; - let cell_id = service.allocate_cell_id().await; - let request_id = service.allocate_request_id().await; - let process_slot = service - .ensure_started() - .await - .map_err(|err| FunctionCallError::RespondToModel(err.to_string()))?; + let stored_values = exec + .session + .services + .code_mode_service + .stored_values() + .await; let started_at = std::time::Instant::now(); - let message = HostToNodeMessage::Start { - request_id: request_id.clone(), - cell_id: cell_id.clone(), - tool_call_id: call_id, - default_yield_time_ms: super::DEFAULT_EXEC_YIELD_TIME_MS, - enabled_tools, - stored_values, - source, - yield_time_ms: args.yield_time_ms, - max_output_tokens: args.max_output_tokens, - }; - let result = { - let mut process_slot = process_slot; - let Some(process) = process_slot.as_mut() else { - return Err(FunctionCallError::RespondToModel(format!( - "{PUBLIC_TOOL_NAME} runner failed to start" - ))); - }; - let message = process - .send(&request_id, &message) - .await - .map_err(|err| err.to_string()); - let message = match message { - Ok(message) => message, - Err(error) => return Err(FunctionCallError::RespondToModel(error)), - }; - handle_node_message( - &exec, cell_id, message, /*poll_max_output_tokens*/ None, started_at, - ) + let response = exec + .session + .services + .code_mode_service + .execute(codex_code_mode::ExecuteRequest { + tool_call_id: call_id, + enabled_tools, + source: args.code, + stored_values, + yield_time_ms: args.yield_time_ms, + max_output_tokens: args.max_output_tokens, + }) .await - }; - match result { - Ok(CodeModeSessionProgress::Finished(output)) - | Ok(CodeModeSessionProgress::Yielded { output }) => Ok(output), - Err(error) => Err(FunctionCallError::RespondToModel(error)), - } + .map_err(FunctionCallError::RespondToModel)?; + handle_runtime_response(&exec, response, args.max_output_tokens, started_at) + .await + .map_err(FunctionCallError::RespondToModel) } } -fn parse_freeform_args(input: &str) -> Result { - if input.trim().is_empty() { - return Err(FunctionCallError::RespondToModel( - "exec expects raw JavaScript source text (non-empty). Provide JS only, optionally with first-line `// @exec: {\"yield_time_ms\": 10000, \"max_output_tokens\": 1000}`.".to_string(), - )); - } - - let mut args = CodeModeExecArgs { - code: input.to_string(), - yield_time_ms: None, - max_output_tokens: None, - }; - - let mut lines = input.splitn(2, '\n'); - let first_line = lines.next().unwrap_or_default(); - let rest = lines.next().unwrap_or_default(); - let trimmed = first_line.trim_start(); - let Some(pragma) = trimmed.strip_prefix(CODE_MODE_PRAGMA_PREFIX) else { - return Ok(args); - }; - - if rest.trim().is_empty() { - return Err(FunctionCallError::RespondToModel( - "exec pragma must be followed by JavaScript source on subsequent lines".to_string(), - )); - } - - let directive = pragma.trim(); - if directive.is_empty() { - return Err(FunctionCallError::RespondToModel( - "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" - .to_string(), - )); - } - - let value: serde_json::Value = serde_json::from_str(directive).map_err(|err| { - FunctionCallError::RespondToModel(format!( - "exec pragma must be valid JSON with supported fields `yield_time_ms` and `max_output_tokens`: {err}" - )) - })?; - let object = value.as_object().ok_or_else(|| { - FunctionCallError::RespondToModel( - "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" - .to_string(), - ) - })?; - for key in object.keys() { - match key.as_str() { - "yield_time_ms" | "max_output_tokens" => {} - _ => { - return Err(FunctionCallError::RespondToModel(format!( - "exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `{key}`" - ))); - } - } - } - - let pragma: CodeModeExecPragma = serde_json::from_value(value).map_err(|err| { - FunctionCallError::RespondToModel(format!( - "exec pragma fields `yield_time_ms` and `max_output_tokens` must be non-negative safe integers: {err}" - )) - })?; - if pragma - .yield_time_ms - .is_some_and(|yield_time_ms| yield_time_ms > MAX_JS_SAFE_INTEGER) - { - return Err(FunctionCallError::RespondToModel( - "exec pragma field `yield_time_ms` must be a non-negative safe integer".to_string(), - )); - } - if pragma.max_output_tokens.is_some_and(|max_output_tokens| { - u64::try_from(max_output_tokens) - .map(|max_output_tokens| max_output_tokens > MAX_JS_SAFE_INTEGER) - .unwrap_or(true) - }) { - return Err(FunctionCallError::RespondToModel( - "exec pragma field `max_output_tokens` must be a non-negative safe integer".to_string(), - )); - } - args.code = rest.to_string(); - args.yield_time_ms = pragma.yield_time_ms; - args.max_output_tokens = pragma.max_output_tokens; - Ok(args) -} - #[async_trait] impl ToolHandler for CodeModeExecuteHandler { type Output = FunctionToolOutput; @@ -216,7 +85,3 @@ impl ToolHandler for CodeModeExecuteHandler { } } } - -#[cfg(test)] -#[path = "execute_handler_tests.rs"] -mod execute_handler_tests; diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index c8e1e0c165..a4838d2463 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -1,15 +1,18 @@ mod execute_handler; -mod process; -mod protocol; -mod service; +mod response_adapter; mod wait_handler; -mod worker; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use codex_code_mode::CodeModeTurnHost; +use codex_code_mode::RuntimeResponse; use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::ResponseInputItem; use serde_json::Value as JsonValue; +use tokio_util::sync::CancellationToken; use crate::client_common::tools::ToolSpec; use crate::codex::Session; @@ -17,9 +20,8 @@ use crate::codex::TurnContext; use crate::function_tool::FunctionCallError; use crate::tools::ToolRouter; use crate::tools::code_mode_description::augment_tool_spec_for_code_mode; -use crate::tools::code_mode_description::code_mode_tool_reference; -use crate::tools::code_mode_description::normalize_code_mode_identifier; use crate::tools::context::FunctionToolOutput; +use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolPayload; use crate::tools::parallel::ToolCallRuntime; use crate::tools::router::ToolCall; @@ -29,180 +31,202 @@ use crate::truncate::TruncationPolicy; use crate::truncate::formatted_truncate_text_content_items_with_policy; use crate::truncate::truncate_function_output_items_with_policy; use crate::unified_exec::resolve_max_tokens; +use codex_features::Feature; -const CODE_MODE_RUNNER_SOURCE: &str = include_str!("runner.cjs"); -const CODE_MODE_BRIDGE_SOURCE: &str = include_str!("bridge.js"); -const CODE_MODE_DESCRIPTION_TEMPLATE: &str = include_str!("description.md"); -const CODE_MODE_WAIT_DESCRIPTION_TEMPLATE: &str = include_str!("wait_description.md"); -const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:"; -const CODE_MODE_ONLY_PREFACE: &str = - "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly"; +pub(crate) use execute_handler::CodeModeExecuteHandler; +use response_adapter::into_function_call_output_content_items; +pub(crate) use wait_handler::CodeModeWaitHandler; -pub(crate) const PUBLIC_TOOL_NAME: &str = "exec"; -pub(crate) const WAIT_TOOL_NAME: &str = "wait"; - -pub(crate) fn is_code_mode_nested_tool(tool_name: &str) -> bool { - tool_name != PUBLIC_TOOL_NAME && tool_name != WAIT_TOOL_NAME -} -pub(crate) const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000; -pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000; +pub(crate) const PUBLIC_TOOL_NAME: &str = codex_code_mode::PUBLIC_TOOL_NAME; +pub(crate) const WAIT_TOOL_NAME: &str = codex_code_mode::WAIT_TOOL_NAME; +pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = codex_code_mode::DEFAULT_WAIT_YIELD_TIME_MS; #[derive(Clone)] -pub(super) struct ExecContext { +pub(crate) struct ExecContext { pub(super) session: Arc, pub(super) turn: Arc, } -pub(crate) use execute_handler::CodeModeExecuteHandler; -pub(crate) use service::CodeModeService; -pub(crate) use wait_handler::CodeModeWaitHandler; - -enum CodeModeSessionProgress { - Finished(FunctionToolOutput), - Yielded { output: FunctionToolOutput }, +pub(crate) struct CodeModeService { + inner: codex_code_mode::CodeModeService, } -enum CodeModeExecutionStatus { - Completed, - Failed, - Running(String), - Terminated, -} - -pub(crate) fn tool_description(enabled_tools: &[(String, String)], code_mode_only: bool) -> String { - let description_template = CODE_MODE_DESCRIPTION_TEMPLATE.trim_end(); - if !code_mode_only { - return description_template.to_string(); +impl CodeModeService { + pub(crate) fn new(_js_repl_node_path: Option) -> Self { + Self { + inner: codex_code_mode::CodeModeService::new(), + } } - let mut sections = vec![ - CODE_MODE_ONLY_PREFACE.to_string(), - description_template.to_string(), - ]; + pub(crate) async fn stored_values(&self) -> std::collections::HashMap { + self.inner.stored_values().await + } - if !enabled_tools.is_empty() { - let nested_tool_reference = enabled_tools - .iter() - .map(|(name, nested_description)| { - let global_name = normalize_code_mode_identifier(name); - format!( - "### `{global_name}` (`{name}`)\n{}", - nested_description.trim() - ) + pub(crate) async fn replace_stored_values( + &self, + values: std::collections::HashMap, + ) { + self.inner.replace_stored_values(values).await; + } + + pub(crate) async fn execute( + &self, + request: codex_code_mode::ExecuteRequest, + ) -> Result { + self.inner.execute(request).await + } + + pub(crate) async fn wait( + &self, + request: codex_code_mode::WaitRequest, + ) -> Result { + self.inner.wait(request).await + } + + pub(crate) async fn start_turn_worker( + &self, + session: &Arc, + turn: &Arc, + router: Arc, + tracker: SharedTurnDiffTracker, + ) -> Option { + if !turn.features.enabled(Feature::CodeMode) { + return None; + } + + let exec = ExecContext { + session: Arc::clone(session), + turn: Arc::clone(turn), + }; + let tool_runtime = + ToolCallRuntime::new(router, Arc::clone(session), Arc::clone(turn), tracker); + let host = Arc::new(CoreTurnHost { exec, tool_runtime }); + Some(self.inner.start_turn_worker(host)) + } +} + +struct CoreTurnHost { + exec: ExecContext, + tool_runtime: ToolCallRuntime, +} + +#[async_trait::async_trait] +impl CodeModeTurnHost for CoreTurnHost { + async fn invoke_tool( + &self, + tool_name: String, + input: Option, + cancellation_token: CancellationToken, + ) -> Result { + call_nested_tool( + self.exec.clone(), + self.tool_runtime.clone(), + tool_name, + input, + cancellation_token, + ) + .await + .map_err(|error| error.to_string()) + } + + async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String> { + if text.trim().is_empty() { + return Ok(()); + } + self.exec + .session + .inject_response_items(vec![ResponseInputItem::CustomToolCallOutput { + call_id, + name: Some(PUBLIC_TOOL_NAME.to_string()), + output: FunctionCallOutputPayload::from_text(text), + }]) + .await + .map_err(|_| { + format!("failed to inject exec notify message for cell {cell_id}: no active turn") }) - .collect::>() - .join("\n\n"); - sections.push(nested_tool_reference); } - - sections.join("\n\n") } -pub(crate) fn wait_tool_description() -> &'static str { - CODE_MODE_WAIT_DESCRIPTION_TEMPLATE -} - -async fn handle_node_message( +pub(super) async fn handle_runtime_response( exec: &ExecContext, - cell_id: String, - message: protocol::NodeToHostMessage, - poll_max_output_tokens: Option>, + response: RuntimeResponse, + max_output_tokens: Option, started_at: std::time::Instant, -) -> Result { - match message { - protocol::NodeToHostMessage::ToolCall { .. } => Err(protocol::unexpected_tool_call_error()), - protocol::NodeToHostMessage::Notify { .. } => Err(format!( - "unexpected {PUBLIC_TOOL_NAME} notify message in response path" - )), - protocol::NodeToHostMessage::Yielded { content_items, .. } => { - let mut delta_items = output_content_items_from_json_values(content_items)?; - delta_items = truncate_code_mode_result(delta_items, poll_max_output_tokens.flatten()); - prepend_script_status( - &mut delta_items, - CodeModeExecutionStatus::Running(cell_id), - started_at.elapsed(), - ); - Ok(CodeModeSessionProgress::Yielded { - output: FunctionToolOutput::from_content(delta_items, Some(true)), - }) +) -> Result { + let script_status = format_script_status(&response); + + match response { + RuntimeResponse::Yielded { content_items, .. } => { + let mut content_items = into_function_call_output_content_items(content_items); + content_items = truncate_code_mode_result(content_items, max_output_tokens); + prepend_script_status(&mut content_items, &script_status, started_at.elapsed()); + Ok(FunctionToolOutput::from_content(content_items, Some(true))) } - protocol::NodeToHostMessage::Terminated { content_items, .. } => { - let mut delta_items = output_content_items_from_json_values(content_items)?; - delta_items = truncate_code_mode_result(delta_items, poll_max_output_tokens.flatten()); - prepend_script_status( - &mut delta_items, - CodeModeExecutionStatus::Terminated, - started_at.elapsed(), - ); - Ok(CodeModeSessionProgress::Finished( - FunctionToolOutput::from_content(delta_items, Some(true)), - )) + RuntimeResponse::Terminated { content_items, .. } => { + let mut content_items = into_function_call_output_content_items(content_items); + content_items = truncate_code_mode_result(content_items, max_output_tokens); + prepend_script_status(&mut content_items, &script_status, started_at.elapsed()); + Ok(FunctionToolOutput::from_content(content_items, Some(true))) } - protocol::NodeToHostMessage::Result { + RuntimeResponse::Result { content_items, stored_values, error_text, - max_output_tokens_per_exec_call, .. } => { + let mut content_items = into_function_call_output_content_items(content_items); exec.session .services .code_mode_service .replace_stored_values(stored_values) .await; - let mut delta_items = output_content_items_from_json_values(content_items)?; let success = error_text.is_none(); if let Some(error_text) = error_text { - delta_items.push(FunctionCallOutputContentItem::InputText { + content_items.push(FunctionCallOutputContentItem::InputText { text: format!("Script error:\n{error_text}"), }); } - - let mut delta_items = truncate_code_mode_result( - delta_items, - poll_max_output_tokens.unwrap_or(max_output_tokens_per_exec_call), - ); - prepend_script_status( - &mut delta_items, - if success { - CodeModeExecutionStatus::Completed - } else { - CodeModeExecutionStatus::Failed - }, - started_at.elapsed(), - ); - Ok(CodeModeSessionProgress::Finished( - FunctionToolOutput::from_content(delta_items, Some(success)), + content_items = truncate_code_mode_result(content_items, max_output_tokens); + prepend_script_status(&mut content_items, &script_status, started_at.elapsed()); + Ok(FunctionToolOutput::from_content( + content_items, + Some(success), )) } } } +fn format_script_status(response: &RuntimeResponse) -> String { + match response { + RuntimeResponse::Yielded { cell_id, .. } => { + format!("Script running with cell ID {cell_id}") + } + RuntimeResponse::Terminated { .. } => "Script terminated".to_string(), + RuntimeResponse::Result { error_text, .. } => { + if error_text.is_none() { + "Script completed".to_string() + } else { + "Script failed".to_string() + } + } + } +} + fn prepend_script_status( content_items: &mut Vec, - status: CodeModeExecutionStatus, + status: &str, wall_time: Duration, ) { let wall_time_seconds = ((wall_time.as_secs_f32()) * 10.0).round() / 10.0; - let header = format!( - "{}\nWall time {wall_time_seconds:.1} seconds\nOutput:\n", - match status { - CodeModeExecutionStatus::Completed => "Script completed".to_string(), - CodeModeExecutionStatus::Failed => "Script failed".to_string(), - CodeModeExecutionStatus::Running(cell_id) => { - format!("Script running with cell ID {cell_id}") - } - CodeModeExecutionStatus::Terminated => "Script terminated".to_string(), - } - ); + let header = format!("{status}\nWall time {wall_time_seconds:.1} seconds\nOutput:\n"); content_items.insert(0, FunctionCallOutputContentItem::InputText { text: header }); } fn truncate_code_mode_result( items: Vec, - max_output_tokens_per_exec_call: Option, + max_output_tokens: Option, ) -> Vec { - let max_output_tokens = resolve_max_tokens(max_output_tokens_per_exec_call); + let max_output_tokens = resolve_max_tokens(max_output_tokens); let policy = TruncationPolicy::Tokens(max_output_tokens); if items .iter() @@ -216,21 +240,9 @@ fn truncate_code_mode_result( truncate_function_output_items_with_policy(&items, policy) } -fn output_content_items_from_json_values( - content_items: Vec, -) -> Result, String> { - content_items - .into_iter() - .enumerate() - .map(|(index, item)| { - serde_json::from_value(item).map_err(|err| { - format!("invalid {PUBLIC_TOOL_NAME} content item at index {index}: {err}") - }) - }) - .collect() -} - -async fn build_enabled_tools(exec: &ExecContext) -> Vec { +pub(super) async fn build_enabled_tools( + exec: &ExecContext, +) -> Vec { let router = build_nested_router(exec).await; let mut out = router .specs() @@ -238,39 +250,37 @@ async fn build_enabled_tools(exec: &ExecContext) -> Vec { .map(|spec| augment_tool_spec_for_code_mode(spec, /*code_mode_enabled*/ true)) .filter_map(enabled_tool_from_spec) .collect::>(); - out.sort_by(|left, right| left.tool_name.cmp(&right.tool_name)); - out.dedup_by(|left, right| left.tool_name == right.tool_name); + out.sort_by(|left, right| left.name.cmp(&right.name)); + out.dedup_by(|left, right| left.name == right.name); out } -fn enabled_tool_from_spec(spec: ToolSpec) -> Option { +fn enabled_tool_from_spec(spec: ToolSpec) -> Option { let tool_name = spec.name().to_string(); - if !is_code_mode_nested_tool(&tool_name) { + if !codex_code_mode::is_code_mode_nested_tool(&tool_name) { return None; } - let reference = code_mode_tool_reference(&tool_name); - let global_name = normalize_code_mode_identifier(&tool_name); - let (description, kind) = match spec { - ToolSpec::Function(tool) => (tool.description, protocol::CodeModeToolKind::Function), - ToolSpec::Freeform(tool) => (tool.description, protocol::CodeModeToolKind::Freeform), + match spec { + ToolSpec::Function(tool) => Some(codex_code_mode::ToolDefinition { + name: tool_name, + description: tool.description, + kind: codex_code_mode::CodeModeToolKind::Function, + input_schema: serde_json::to_value(&tool.parameters).ok(), + output_schema: tool.output_schema, + }), + ToolSpec::Freeform(tool) => Some(codex_code_mode::ToolDefinition { + name: tool_name, + description: tool.description, + kind: codex_code_mode::CodeModeToolKind::Freeform, + input_schema: None, + output_schema: None, + }), ToolSpec::LocalShell {} | ToolSpec::ImageGeneration { .. } | ToolSpec::ToolSearch { .. } - | ToolSpec::WebSearch { .. } => { - return None; - } - }; - - Some(protocol::EnabledTool { - tool_name, - global_name, - module_path: reference.module_path, - namespace: reference.namespace, - name: normalize_code_mode_identifier(&reference.tool_key), - description, - kind, - }) + | ToolSpec::WebSearch { .. } => None, + } } async fn build_nested_router(exec: &ExecContext) -> ToolRouter { @@ -303,7 +313,7 @@ async fn call_nested_tool( tool_runtime: ToolCallRuntime, tool_name: String, input: Option, - cancellation_token: tokio_util::sync::CancellationToken, + cancellation_token: CancellationToken, ) -> Result { if tool_name == PUBLIC_TOOL_NAME { return Err(FunctionCallError::RespondToModel(format!( @@ -340,18 +350,18 @@ async fn call_nested_tool( Ok(result.code_mode_result()) } -fn tool_kind_for_spec(spec: &ToolSpec) -> protocol::CodeModeToolKind { +fn tool_kind_for_spec(spec: &ToolSpec) -> codex_code_mode::CodeModeToolKind { if matches!(spec, ToolSpec::Freeform(_)) { - protocol::CodeModeToolKind::Freeform + codex_code_mode::CodeModeToolKind::Freeform } else { - protocol::CodeModeToolKind::Function + codex_code_mode::CodeModeToolKind::Function } } fn tool_kind_for_name( spec: Option, tool_name: &str, -) -> Result { +) -> Result { spec.as_ref() .map(tool_kind_for_spec) .ok_or_else(|| format!("tool `{tool_name}` is not enabled in {PUBLIC_TOOL_NAME}")) @@ -364,8 +374,12 @@ fn build_nested_tool_payload( ) -> Result { let actual_kind = tool_kind_for_name(spec, tool_name)?; match actual_kind { - protocol::CodeModeToolKind::Function => build_function_tool_payload(tool_name, input), - protocol::CodeModeToolKind::Freeform => build_freeform_tool_payload(tool_name, input), + codex_code_mode::CodeModeToolKind::Function => { + build_function_tool_payload(tool_name, input) + } + codex_code_mode::CodeModeToolKind::Freeform => { + build_freeform_tool_payload(tool_name, input) + } } } diff --git a/codex-rs/core/src/tools/code_mode/process.rs b/codex-rs/core/src/tools/code_mode/process.rs deleted file mode 100644 index 6dd6cde3ae..0000000000 --- a/codex-rs/core/src/tools/code_mode/process.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use tokio::io::AsyncBufReadExt; -use tokio::io::AsyncReadExt; -use tokio::io::AsyncWriteExt; -use tokio::io::BufReader; -use tokio::sync::Mutex; -use tokio::sync::mpsc; -use tokio::sync::oneshot; -use tokio::task::JoinHandle; -use tracing::warn; - -use super::CODE_MODE_RUNNER_SOURCE; -use super::PUBLIC_TOOL_NAME; -use super::protocol::HostToNodeMessage; -use super::protocol::NodeToHostMessage; -use super::protocol::message_request_id; - -pub(super) struct CodeModeProcess { - pub(super) child: tokio::process::Child, - pub(super) stdin: Arc>, - pub(super) stdout_task: JoinHandle<()>, - pub(super) response_waiters: Arc>>>, - pub(super) message_rx: Arc>>, -} - -impl CodeModeProcess { - pub(super) async fn send( - &mut self, - request_id: &str, - message: &HostToNodeMessage, - ) -> Result { - if self.stdout_task.is_finished() { - return Err(std::io::Error::other(format!( - "{PUBLIC_TOOL_NAME} runner is not available" - ))); - } - - let (tx, rx) = oneshot::channel(); - self.response_waiters - .lock() - .await - .insert(request_id.to_string(), tx); - if let Err(err) = write_message(&self.stdin, message).await { - self.response_waiters.lock().await.remove(request_id); - return Err(err); - } - - match rx.await { - Ok(message) => Ok(message), - Err(_) => Err(std::io::Error::other(format!( - "{PUBLIC_TOOL_NAME} runner is not available" - ))), - } - } - - pub(super) fn has_exited(&mut self) -> Result { - self.child - .try_wait() - .map(|status| status.is_some()) - .map_err(std::io::Error::other) - } -} - -pub(super) async fn spawn_code_mode_process( - node_path: &std::path::Path, -) -> Result { - let mut cmd = tokio::process::Command::new(node_path); - cmd.arg("--experimental-vm-modules"); - cmd.arg("--eval"); - cmd.arg(CODE_MODE_RUNNER_SOURCE); - cmd.stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .kill_on_drop(true); - - let mut child = cmd.spawn().map_err(std::io::Error::other)?; - let stdout = child.stdout.take().ok_or_else(|| { - std::io::Error::other(format!("{PUBLIC_TOOL_NAME} runner missing stdout")) - })?; - let stderr = child.stderr.take().ok_or_else(|| { - std::io::Error::other(format!("{PUBLIC_TOOL_NAME} runner missing stderr")) - })?; - let stdin = child - .stdin - .take() - .ok_or_else(|| std::io::Error::other(format!("{PUBLIC_TOOL_NAME} runner missing stdin")))?; - let stdin = Arc::new(Mutex::new(stdin)); - let response_waiters = Arc::new(Mutex::new(HashMap::< - String, - oneshot::Sender, - >::new())); - let (message_tx, message_rx) = mpsc::unbounded_channel(); - - tokio::spawn(async move { - let mut reader = BufReader::new(stderr); - let mut buf = Vec::new(); - match reader.read_to_end(&mut buf).await { - Ok(_) => { - let stderr = String::from_utf8_lossy(&buf).trim().to_string(); - if !stderr.is_empty() { - warn!("{PUBLIC_TOOL_NAME} runner stderr: {stderr}"); - } - } - Err(err) => { - warn!("failed to read {PUBLIC_TOOL_NAME} stderr: {err}"); - } - } - }); - let stdout_task = tokio::spawn({ - let response_waiters = Arc::clone(&response_waiters); - async move { - let mut stdout_lines = BufReader::new(stdout).lines(); - loop { - let line = match stdout_lines.next_line().await { - Ok(line) => line, - Err(err) => { - warn!("failed to read {PUBLIC_TOOL_NAME} stdout: {err}"); - break; - } - }; - let Some(line) = line else { - break; - }; - if line.trim().is_empty() { - continue; - } - let message: NodeToHostMessage = match serde_json::from_str(&line) { - Ok(message) => message, - Err(err) => { - warn!("failed to parse {PUBLIC_TOOL_NAME} stdout message: {err}"); - break; - } - }; - match message { - message @ (NodeToHostMessage::ToolCall { .. } - | NodeToHostMessage::Notify { .. }) => { - let _ = message_tx.send(message); - } - message => { - if let Some(request_id) = message_request_id(&message) - && let Some(waiter) = response_waiters.lock().await.remove(request_id) - { - let _ = waiter.send(message); - } - } - } - } - response_waiters.lock().await.clear(); - } - }); - - Ok(CodeModeProcess { - child, - stdin, - stdout_task, - response_waiters, - message_rx: Arc::new(Mutex::new(message_rx)), - }) -} - -pub(super) async fn write_message( - stdin: &Arc>, - message: &HostToNodeMessage, -) -> Result<(), std::io::Error> { - let line = serde_json::to_string(message).map_err(std::io::Error::other)?; - let mut stdin = stdin.lock().await; - stdin.write_all(line.as_bytes()).await?; - stdin.write_all(b"\n").await?; - stdin.flush().await?; - Ok(()) -} diff --git a/codex-rs/core/src/tools/code_mode/protocol.rs b/codex-rs/core/src/tools/code_mode/protocol.rs deleted file mode 100644 index 2e72e1229c..0000000000 --- a/codex-rs/core/src/tools/code_mode/protocol.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::collections::HashMap; - -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value as JsonValue; - -use super::CODE_MODE_BRIDGE_SOURCE; -use super::PUBLIC_TOOL_NAME; - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub(super) enum CodeModeToolKind { - Function, - Freeform, -} - -#[derive(Clone, Debug, Serialize)] -pub(super) struct EnabledTool { - pub(super) tool_name: String, - pub(super) global_name: String, - #[serde(rename = "module")] - pub(super) module_path: String, - pub(super) namespace: Vec, - pub(super) name: String, - pub(super) description: String, - pub(super) kind: CodeModeToolKind, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "snake_case")] -pub(super) struct CodeModeToolCall { - pub(super) request_id: String, - pub(super) id: String, - pub(super) name: String, - #[serde(default)] - pub(super) input: Option, -} - -#[derive(Clone, Debug, Deserialize)] -pub(super) struct CodeModeNotify { - pub(super) cell_id: String, - pub(super) call_id: String, - pub(super) text: String, -} - -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub(super) enum HostToNodeMessage { - Start { - request_id: String, - cell_id: String, - tool_call_id: String, - default_yield_time_ms: u64, - enabled_tools: Vec, - stored_values: HashMap, - source: String, - yield_time_ms: Option, - max_output_tokens: Option, - }, - Poll { - request_id: String, - cell_id: String, - yield_time_ms: u64, - }, - Terminate { - request_id: String, - cell_id: String, - }, - Response { - request_id: String, - id: String, - code_mode_result: JsonValue, - #[serde(default)] - error_text: Option, - }, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub(super) enum NodeToHostMessage { - ToolCall { - #[serde(flatten)] - tool_call: CodeModeToolCall, - }, - Yielded { - request_id: String, - content_items: Vec, - }, - Terminated { - request_id: String, - content_items: Vec, - }, - Notify { - #[serde(flatten)] - notify: CodeModeNotify, - }, - Result { - request_id: String, - content_items: Vec, - stored_values: HashMap, - #[serde(default)] - error_text: Option, - #[serde(default)] - max_output_tokens_per_exec_call: Option, - }, -} - -pub(super) fn build_source( - user_code: &str, - enabled_tools: &[EnabledTool], -) -> Result { - let enabled_tools_json = serde_json::to_string(enabled_tools) - .map_err(|err| format!("failed to serialize enabled tools: {err}"))?; - Ok(CODE_MODE_BRIDGE_SOURCE - .replace( - "__CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__", - &enabled_tools_json, - ) - .replace("__CODE_MODE_USER_CODE_PLACEHOLDER__", user_code)) -} - -pub(super) fn message_request_id(message: &NodeToHostMessage) -> Option<&str> { - match message { - NodeToHostMessage::ToolCall { .. } => None, - NodeToHostMessage::Yielded { request_id, .. } - | NodeToHostMessage::Terminated { request_id, .. } - | NodeToHostMessage::Result { request_id, .. } => Some(request_id), - NodeToHostMessage::Notify { .. } => None, - } -} - -pub(super) fn unexpected_tool_call_error() -> String { - format!("{PUBLIC_TOOL_NAME} received an unexpected tool call response") -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use super::CodeModeNotify; - use super::NodeToHostMessage; - use super::message_request_id; - - #[test] - fn message_request_id_absent_for_notify() { - let message = NodeToHostMessage::Notify { - notify: CodeModeNotify { - cell_id: "1".to_string(), - call_id: "call-1".to_string(), - text: "hello".to_string(), - }, - }; - - assert_eq!(None, message_request_id(&message)); - } - - #[test] - fn message_request_id_present_for_result() { - let message = NodeToHostMessage::Result { - request_id: "req-1".to_string(), - content_items: Vec::new(), - stored_values: HashMap::new(), - error_text: None, - max_output_tokens_per_exec_call: None, - }; - - assert_eq!(Some("req-1"), message_request_id(&message)); - } -} diff --git a/codex-rs/core/src/tools/code_mode/response_adapter.rs b/codex-rs/core/src/tools/code_mode/response_adapter.rs new file mode 100644 index 0000000000..b90448acf9 --- /dev/null +++ b/codex-rs/core/src/tools/code_mode/response_adapter.rs @@ -0,0 +1,44 @@ +use codex_code_mode::ImageDetail as CodeModeImageDetail; +use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::models::ImageDetail; + +trait IntoProtocol { + fn into_protocol(self) -> T; +} + +pub(super) fn into_function_call_output_content_items( + items: Vec, +) -> Vec { + items.into_iter().map(IntoProtocol::into_protocol).collect() +} + +impl IntoProtocol for CodeModeImageDetail { + fn into_protocol(self) -> ImageDetail { + let value = self; + match value { + CodeModeImageDetail::Auto => ImageDetail::Auto, + CodeModeImageDetail::Low => ImageDetail::Low, + CodeModeImageDetail::High => ImageDetail::High, + CodeModeImageDetail::Original => ImageDetail::Original, + } + } +} + +impl IntoProtocol + for codex_code_mode::FunctionCallOutputContentItem +{ + fn into_protocol(self) -> FunctionCallOutputContentItem { + let value = self; + match value { + codex_code_mode::FunctionCallOutputContentItem::InputText { text } => { + FunctionCallOutputContentItem::InputText { text } + } + codex_code_mode::FunctionCallOutputContentItem::InputImage { image_url, detail } => { + FunctionCallOutputContentItem::InputImage { + image_url, + detail: detail.map(IntoProtocol::into_protocol), + } + } + } + } +} diff --git a/codex-rs/core/src/tools/code_mode/runner.cjs b/codex-rs/core/src/tools/code_mode/runner.cjs deleted file mode 100644 index 8b4b322eb3..0000000000 --- a/codex-rs/core/src/tools/code_mode/runner.cjs +++ /dev/null @@ -1,938 +0,0 @@ -'use strict'; - -const readline = require('node:readline'); -const { Worker } = require('node:worker_threads'); - -const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL = 10000; - -function normalizeMaxOutputTokensPerExecCall(value) { - if (!Number.isSafeInteger(value) || value < 0) { - throw new TypeError('max_output_tokens_per_exec_call must be a non-negative safe integer'); - } - return value; -} - -function normalizeYieldTime(value) { - if (!Number.isSafeInteger(value) || value < 0) { - throw new TypeError('yield_time must be a non-negative safe integer'); - } - return value; -} - -function formatErrorText(error) { - return String(error && error.stack ? error.stack : error); -} - -function cloneJsonValue(value) { - return JSON.parse(JSON.stringify(value)); -} - -function clearTimer(timer) { - if (timer !== null) { - clearTimeout(timer); - } - return null; -} - -function takeContentItems(session) { - const clonedContentItems = cloneJsonValue(session.content_items); - session.content_items.splice(0, session.content_items.length); - return Array.isArray(clonedContentItems) ? clonedContentItems : []; -} - -function codeModeWorkerMain() { - 'use strict'; - - const { parentPort, workerData } = require('node:worker_threads'); - const vm = require('node:vm'); - const { SourceTextModule, SyntheticModule } = vm; - - function formatErrorText(error) { - return String(error && error.stack ? error.stack : error); - } - - function cloneJsonValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - class CodeModeExitSignal extends Error { - constructor() { - super('code mode exit'); - this.name = 'CodeModeExitSignal'; - } - } - - function isCodeModeExitSignal(error) { - return error instanceof CodeModeExitSignal; - } - - function createToolCaller() { - let nextId = 0; - const pending = new Map(); - - parentPort.on('message', (message) => { - if (message.type === 'tool_response') { - const entry = pending.get(message.id); - if (!entry) { - return; - } - pending.delete(message.id); - entry.resolve(message.result ?? ''); - return; - } - - if (message.type === 'tool_response_error') { - const entry = pending.get(message.id); - if (!entry) { - return; - } - pending.delete(message.id); - entry.reject(new Error(message.error_text ?? 'tool call failed')); - return; - } - }); - - return (name, input) => { - const id = 'msg-' + ++nextId; - return new Promise((resolve, reject) => { - pending.set(id, { resolve, reject }); - parentPort.postMessage({ - type: 'tool_call', - id, - name: String(name), - input, - }); - }); - }; - } - - function createContentItems() { - const contentItems = []; - const push = contentItems.push.bind(contentItems); - contentItems.push = (...items) => { - for (const item of items) { - parentPort.postMessage({ - type: 'content_item', - item: cloneJsonValue(item), - }); - } - return push(...items); - }; - parentPort.on('message', (message) => { - if (message.type === 'clear_content') { - contentItems.splice(0, contentItems.length); - } - }); - return contentItems; - } - - function createGlobalToolsNamespace(callTool, enabledTools) { - const tools = Object.create(null); - - for (const { tool_name, global_name } of enabledTools) { - Object.defineProperty(tools, global_name, { - value: async (args) => callTool(tool_name, args), - configurable: false, - enumerable: true, - writable: false, - }); - } - - return Object.freeze(tools); - } - - function createModuleToolsNamespace(callTool, enabledTools) { - const tools = Object.create(null); - - for (const { tool_name, global_name } of enabledTools) { - Object.defineProperty(tools, global_name, { - value: async (args) => callTool(tool_name, args), - configurable: false, - enumerable: true, - writable: false, - }); - } - - return Object.freeze(tools); - } - - function createAllToolsMetadata(enabledTools) { - return Object.freeze( - enabledTools.map(({ global_name, description }) => - Object.freeze({ - name: global_name, - description, - }) - ) - ); - } - - function createToolsModule(context, callTool, enabledTools) { - const tools = createModuleToolsNamespace(callTool, enabledTools); - const allTools = createAllToolsMetadata(enabledTools); - const exportNames = ['ALL_TOOLS']; - - for (const { global_name } of enabledTools) { - if (global_name !== 'ALL_TOOLS') { - exportNames.push(global_name); - } - } - - const uniqueExportNames = [...new Set(exportNames)]; - - return new SyntheticModule( - uniqueExportNames, - function initToolsModule() { - this.setExport('ALL_TOOLS', allTools); - for (const exportName of uniqueExportNames) { - if (exportName !== 'ALL_TOOLS') { - this.setExport(exportName, tools[exportName]); - } - } - }, - { context } - ); - } - - function ensureContentItems(context) { - if (!Array.isArray(context.__codexContentItems)) { - context.__codexContentItems = []; - } - return context.__codexContentItems; - } - - function serializeOutputText(value) { - if (typeof value === 'string') { - return value; - } - if ( - typeof value === 'undefined' || - value === null || - typeof value === 'boolean' || - typeof value === 'number' || - typeof value === 'bigint' - ) { - return String(value); - } - - const serialized = JSON.stringify(value); - if (typeof serialized === 'string') { - return serialized; - } - - return String(value); - } - - function normalizeOutputImage(value) { - let imageUrl; - let detail; - if (typeof value === 'string') { - imageUrl = value; - } else if ( - value && - typeof value === 'object' && - !Array.isArray(value) - ) { - if (typeof value.image_url === 'string') { - imageUrl = value.image_url; - } - if (typeof value.detail === 'string') { - detail = value.detail; - } else if ( - Object.prototype.hasOwnProperty.call(value, 'detail') && - value.detail !== null && - typeof value.detail !== 'undefined' - ) { - throw new TypeError('image detail must be a string when provided'); - } - } - - if (typeof imageUrl !== 'string' || !imageUrl) { - throw new TypeError( - 'image expects a non-empty image URL string or an object with image_url and optional detail' - ); - } - if (!/^(?:https?:\/\/|data:)/i.test(imageUrl)) { - throw new TypeError('image expects an http(s) or data URL'); - } - - if (typeof detail !== 'undefined' && !/^(?:auto|low|high|original)$/i.test(detail)) { - throw new TypeError('image detail must be one of: auto, low, high, original'); - } - - const normalized = { image_url: imageUrl }; - if (typeof detail === 'string') { - normalized.detail = detail.toLowerCase(); - } - return normalized; - } - - function createCodeModeHelpers(context, state, toolCallId) { - const load = (key) => { - if (typeof key !== 'string') { - throw new TypeError('load key must be a string'); - } - if (!Object.prototype.hasOwnProperty.call(state.storedValues, key)) { - return undefined; - } - return cloneJsonValue(state.storedValues[key]); - }; - const store = (key, value) => { - if (typeof key !== 'string') { - throw new TypeError('store key must be a string'); - } - state.storedValues[key] = cloneJsonValue(value); - }; - const text = (value) => { - const item = { - type: 'input_text', - text: serializeOutputText(value), - }; - ensureContentItems(context).push(item); - return item; - }; - const image = (value) => { - const item = Object.assign({ type: 'input_image' }, normalizeOutputImage(value)); - ensureContentItems(context).push(item); - return item; - }; - const yieldControl = () => { - parentPort.postMessage({ type: 'yield' }); - }; - const notify = (value) => { - const text = serializeOutputText(value); - if (text.trim().length === 0) { - throw new TypeError('notify expects non-empty text'); - } - if (typeof toolCallId !== 'string' || toolCallId.length === 0) { - throw new TypeError('notify requires a valid tool call id'); - } - parentPort.postMessage({ - type: 'notify', - call_id: toolCallId, - text, - }); - return text; - }; - const exit = () => { - throw new CodeModeExitSignal(); - }; - - return Object.freeze({ - exit, - image, - load, - notify, - output_image: image, - output_text: text, - store, - text, - yield_control: yieldControl, - }); - } - - function createCodeModeModule(context, helpers) { - return new SyntheticModule( - [ - 'exit', - 'image', - 'load', - 'notify', - 'output_text', - 'output_image', - 'store', - 'text', - 'yield_control', - ], - function initCodeModeModule() { - this.setExport('exit', helpers.exit); - this.setExport('image', helpers.image); - this.setExport('load', helpers.load); - this.setExport('notify', helpers.notify); - this.setExport('output_text', helpers.output_text); - this.setExport('output_image', helpers.output_image); - this.setExport('store', helpers.store); - this.setExport('text', helpers.text); - this.setExport('yield_control', helpers.yield_control); - }, - { context } - ); - } - - function createBridgeRuntime(callTool, enabledTools, helpers) { - return Object.freeze({ - ALL_TOOLS: createAllToolsMetadata(enabledTools), - exit: helpers.exit, - image: helpers.image, - load: helpers.load, - notify: helpers.notify, - store: helpers.store, - text: helpers.text, - tools: createGlobalToolsNamespace(callTool, enabledTools), - yield_control: helpers.yield_control, - }); - } - - function namespacesMatch(left, right) { - if (left.length !== right.length) { - return false; - } - return left.every((segment, index) => segment === right[index]); - } - - function createNamespacedToolsNamespace(callTool, enabledTools, namespace) { - const tools = Object.create(null); - - for (const tool of enabledTools) { - const toolNamespace = Array.isArray(tool.namespace) ? tool.namespace : []; - if (!namespacesMatch(toolNamespace, namespace)) { - continue; - } - - Object.defineProperty(tools, tool.name, { - value: async (args) => callTool(tool.tool_name, args), - configurable: false, - enumerable: true, - writable: false, - }); - } - - return Object.freeze(tools); - } - - function createNamespacedToolsModule(context, callTool, enabledTools, namespace) { - const tools = createNamespacedToolsNamespace(callTool, enabledTools, namespace); - const exportNames = []; - - for (const exportName of Object.keys(tools)) { - if (exportName !== 'ALL_TOOLS') { - exportNames.push(exportName); - } - } - - const uniqueExportNames = [...new Set(exportNames)]; - - return new SyntheticModule( - uniqueExportNames, - function initNamespacedToolsModule() { - for (const exportName of uniqueExportNames) { - this.setExport(exportName, tools[exportName]); - } - }, - { context } - ); - } - - function createModuleResolver(context, callTool, enabledTools, helpers) { - let toolsModule; - let codeModeModule; - const namespacedModules = new Map(); - - return function resolveModule(specifier) { - if (specifier === 'tools.js') { - toolsModule ??= createToolsModule(context, callTool, enabledTools); - return toolsModule; - } - if (specifier === '@openai/code_mode' || specifier === 'openai/code_mode') { - codeModeModule ??= createCodeModeModule(context, helpers); - return codeModeModule; - } - const namespacedMatch = /^tools\/(.+)\.js$/.exec(specifier); - if (!namespacedMatch) { - throw new Error('Unsupported import in exec: ' + specifier); - } - - const namespace = namespacedMatch[1] - .split('/') - .filter((segment) => segment.length > 0); - if (namespace.length === 0) { - throw new Error('Unsupported import in exec: ' + specifier); - } - - const cacheKey = namespace.join('/'); - if (!namespacedModules.has(cacheKey)) { - namespacedModules.set( - cacheKey, - createNamespacedToolsModule(context, callTool, enabledTools, namespace) - ); - } - return namespacedModules.get(cacheKey); - }; - } - - async function resolveDynamicModule(specifier, resolveModule) { - const module = resolveModule(specifier); - - if (module.status === 'unlinked') { - await module.link(resolveModule); - } - - if (module.status === 'linked' || module.status === 'evaluating') { - await module.evaluate(); - } - - if (module.status === 'errored') { - throw module.error; - } - - return module; - } - - async function runModule(context, start, callTool, helpers) { - const resolveModule = createModuleResolver( - context, - callTool, - start.enabled_tools ?? [], - helpers - ); - const mainModule = new SourceTextModule(start.source, { - context, - identifier: 'exec_main.mjs', - importModuleDynamically: async (specifier) => - resolveDynamicModule(specifier, resolveModule), - }); - - await mainModule.link(resolveModule); - await mainModule.evaluate(); - } - - async function main() { - const start = workerData ?? {}; - const toolCallId = start.tool_call_id; - const state = { - storedValues: cloneJsonValue(start.stored_values ?? {}), - }; - const callTool = createToolCaller(); - const enabledTools = start.enabled_tools ?? []; - const contentItems = createContentItems(); - const context = vm.createContext({ - __codexContentItems: contentItems, - }); - const helpers = createCodeModeHelpers(context, state, toolCallId); - Object.defineProperty(context, '__codexRuntime', { - value: createBridgeRuntime(callTool, enabledTools, helpers), - configurable: true, - enumerable: false, - writable: false, - }); - - parentPort.postMessage({ type: 'started' }); - try { - await runModule(context, start, callTool, helpers); - parentPort.postMessage({ - type: 'result', - stored_values: state.storedValues, - }); - } catch (error) { - if (isCodeModeExitSignal(error)) { - parentPort.postMessage({ - type: 'result', - stored_values: state.storedValues, - }); - return; - } - parentPort.postMessage({ - type: 'result', - stored_values: state.storedValues, - error_text: formatErrorText(error), - }); - } - } - - void main().catch((error) => { - parentPort.postMessage({ - type: 'result', - stored_values: {}, - error_text: formatErrorText(error), - }); - }); -} - -function createProtocol() { - const rl = readline.createInterface({ - input: process.stdin, - crlfDelay: Infinity, - }); - - let nextId = 0; - const pending = new Map(); - const sessions = new Map(); - let closedResolve; - const closed = new Promise((resolve) => { - closedResolve = resolve; - }); - - rl.on('line', (line) => { - if (!line.trim()) { - return; - } - - let message; - try { - message = JSON.parse(line); - } catch (error) { - process.stderr.write(formatErrorText(error) + '\n'); - return; - } - - if (message.type === 'start') { - startSession(protocol, sessions, message); - return; - } - - if (message.type === 'poll') { - const session = sessions.get(message.cell_id); - if (session) { - session.request_id = String(message.request_id); - if (session.pending_result) { - void completeSession(protocol, sessions, session, session.pending_result); - } else { - schedulePollYield(protocol, session, normalizeYieldTime(message.yield_time_ms ?? 0)); - } - } else { - void protocol.send({ - type: 'result', - request_id: message.request_id, - content_items: [], - stored_values: {}, - error_text: `exec cell ${message.cell_id} not found`, - max_output_tokens_per_exec_call: DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL, - }); - } - return; - } - - if (message.type === 'terminate') { - const session = sessions.get(message.cell_id); - if (session) { - session.request_id = String(message.request_id); - void terminateSession(protocol, sessions, session); - } else { - void protocol.send({ - type: 'result', - request_id: message.request_id, - content_items: [], - stored_values: {}, - error_text: `exec cell ${message.cell_id} not found`, - max_output_tokens_per_exec_call: DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL, - }); - } - return; - } - - if (message.type === 'response') { - const entry = pending.get(message.request_id + ':' + message.id); - if (!entry) { - return; - } - pending.delete(message.request_id + ':' + message.id); - if (typeof message.error_text === 'string') { - entry.reject(new Error(message.error_text)); - return; - } - entry.resolve(message.code_mode_result ?? ''); - return; - } - - process.stderr.write('Unknown protocol message type: ' + message.type + '\n'); - }); - - rl.on('close', () => { - const error = new Error('stdin closed'); - for (const entry of pending.values()) { - entry.reject(error); - } - pending.clear(); - for (const session of sessions.values()) { - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - void session.worker.terminate().catch(() => {}); - } - sessions.clear(); - closedResolve(); - }); - - function send(message) { - return new Promise((resolve, reject) => { - process.stdout.write(JSON.stringify(message) + '\n', (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } - - function request(type, payload) { - const requestId = 'req-' + ++nextId; - const id = 'msg-' + ++nextId; - const pendingKey = requestId + ':' + id; - return new Promise((resolve, reject) => { - pending.set(pendingKey, { resolve, reject }); - void send({ type, request_id: requestId, id, ...payload }).catch((error) => { - pending.delete(pendingKey); - reject(error); - }); - }); - } - - const protocol = { closed, request, send }; - return protocol; -} - -function sessionWorkerSource() { - return '(' + codeModeWorkerMain.toString() + ')();'; -} - -function startSession(protocol, sessions, start) { - if (typeof start.tool_call_id !== 'string' || start.tool_call_id.length === 0) { - throw new TypeError('start requires a valid tool_call_id'); - } - const maxOutputTokensPerExecCall = - start.max_output_tokens == null - ? DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL - : normalizeMaxOutputTokensPerExecCall(start.max_output_tokens); - const session = { - completed: false, - content_items: [], - default_yield_time_ms: normalizeYieldTime(start.default_yield_time_ms), - id: start.cell_id, - initial_yield_time_ms: - start.yield_time_ms == null - ? normalizeYieldTime(start.default_yield_time_ms) - : normalizeYieldTime(start.yield_time_ms), - initial_yield_timer: null, - initial_yield_triggered: false, - max_output_tokens_per_exec_call: maxOutputTokensPerExecCall, - pending_result: null, - poll_yield_timer: null, - request_id: String(start.request_id), - worker: new Worker(sessionWorkerSource(), { - eval: true, - workerData: start, - }), - }; - sessions.set(session.id, session); - - session.worker.on('message', (message) => { - void handleWorkerMessage(protocol, sessions, session, message).catch((error) => { - void completeSession(protocol, sessions, session, { - type: 'result', - stored_values: {}, - error_text: formatErrorText(error), - }); - }); - }); - session.worker.on('error', (error) => { - void completeSession(protocol, sessions, session, { - type: 'result', - stored_values: {}, - error_text: formatErrorText(error), - }); - }); - session.worker.on('exit', (code) => { - if (code !== 0 && !session.completed) { - void completeSession(protocol, sessions, session, { - type: 'result', - stored_values: {}, - error_text: 'exec worker exited with code ' + code, - }); - } - }); -} - -async function handleWorkerMessage(protocol, sessions, session, message) { - if (session.completed) { - return; - } - - if (message.type === 'content_item') { - session.content_items.push(cloneJsonValue(message.item)); - return; - } - - if (message.type === 'started') { - scheduleInitialYield(protocol, session, session.initial_yield_time_ms); - return; - } - - if (message.type === 'yield') { - void sendYielded(protocol, session); - return; - } - - if (message.type === 'notify') { - if (typeof message.text !== 'string' || message.text.trim().length === 0) { - throw new TypeError('notify requires non-empty text'); - } - if (typeof message.call_id !== 'string' || message.call_id.length === 0) { - throw new TypeError('notify requires a valid call id'); - } - await protocol.send({ - type: 'notify', - cell_id: session.id, - call_id: message.call_id, - text: message.text, - }); - return; - } - - if (message.type === 'tool_call') { - void forwardToolCall(protocol, session, message); - return; - } - - if (message.type === 'result') { - const result = { - type: 'result', - stored_values: cloneJsonValue(message.stored_values ?? {}), - error_text: - typeof message.error_text === 'string' ? message.error_text : undefined, - }; - if (session.request_id === null) { - session.pending_result = result; - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - return; - } - await completeSession(protocol, sessions, session, result); - return; - } - - process.stderr.write('Unknown worker message type: ' + message.type + '\n'); -} - -async function forwardToolCall(protocol, session, message) { - try { - const result = await protocol.request('tool_call', { - name: String(message.name), - input: message.input, - }); - if (session.completed) { - return; - } - try { - session.worker.postMessage({ - type: 'tool_response', - id: message.id, - result, - }); - } catch {} - } catch (error) { - if (session.completed) { - return; - } - try { - session.worker.postMessage({ - type: 'tool_response_error', - id: message.id, - error_text: formatErrorText(error), - }); - } catch {} - } -} - -async function sendYielded(protocol, session) { - if (session.completed || session.request_id === null) { - return; - } - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.initial_yield_triggered = true; - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - const contentItems = takeContentItems(session); - const requestId = session.request_id; - try { - session.worker.postMessage({ type: 'clear_content' }); - } catch {} - await protocol.send({ - type: 'yielded', - request_id: requestId, - content_items: contentItems, - }); - session.request_id = null; -} - -function scheduleInitialYield(protocol, session, yieldTime) { - if (session.completed || session.initial_yield_triggered) { - return yieldTime; - } - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.initial_yield_timer = setTimeout(() => { - session.initial_yield_timer = null; - session.initial_yield_triggered = true; - void sendYielded(protocol, session); - }, yieldTime); - return yieldTime; -} - -function schedulePollYield(protocol, session, yieldTime) { - if (session.completed) { - return; - } - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - session.poll_yield_timer = setTimeout(() => { - session.poll_yield_timer = null; - void sendYielded(protocol, session); - }, yieldTime); -} - -async function completeSession(protocol, sessions, session, message) { - if (session.completed) { - return; - } - if (session.request_id === null) { - session.pending_result = message; - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - return; - } - const requestId = session.request_id; - session.completed = true; - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - sessions.delete(session.id); - const contentItems = takeContentItems(session); - session.pending_result = null; - try { - session.worker.postMessage({ type: 'clear_content' }); - } catch {} - await protocol.send({ - ...message, - request_id: requestId, - content_items: contentItems, - max_output_tokens_per_exec_call: session.max_output_tokens_per_exec_call, - }); -} - -async function terminateSession(protocol, sessions, session) { - if (session.completed) { - return; - } - session.completed = true; - session.initial_yield_timer = clearTimer(session.initial_yield_timer); - session.poll_yield_timer = clearTimer(session.poll_yield_timer); - sessions.delete(session.id); - const contentItems = takeContentItems(session); - try { - await session.worker.terminate(); - } catch {} - await protocol.send({ - type: 'terminated', - request_id: session.request_id, - content_items: contentItems, - }); -} - -async function main() { - const protocol = createProtocol(); - await protocol.closed; -} - -void main().catch(async (error) => { - try { - process.stderr.write(formatErrorText(error) + '\n'); - } finally { - process.exitCode = 1; - } -}); diff --git a/codex-rs/core/src/tools/code_mode/service.rs b/codex-rs/core/src/tools/code_mode/service.rs deleted file mode 100644 index a9fadedb82..0000000000 --- a/codex-rs/core/src/tools/code_mode/service.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; - -use serde_json::Value as JsonValue; -use tokio::sync::Mutex; -use tracing::warn; - -use crate::codex::Session; -use crate::codex::TurnContext; -use crate::tools::ToolRouter; -use crate::tools::context::SharedTurnDiffTracker; -use crate::tools::js_repl::resolve_compatible_node; -use crate::tools::parallel::ToolCallRuntime; -use codex_features::Feature; - -use super::ExecContext; -use super::PUBLIC_TOOL_NAME; -use super::process::CodeModeProcess; -use super::process::spawn_code_mode_process; -use super::worker::CodeModeWorker; - -pub(crate) struct CodeModeService { - js_repl_node_path: Option, - stored_values: Mutex>, - process: Arc>>, - next_cell_id: Mutex, -} - -impl CodeModeService { - pub(crate) fn new(js_repl_node_path: Option) -> Self { - Self { - js_repl_node_path, - stored_values: Mutex::new(HashMap::new()), - process: Arc::new(Mutex::new(None)), - next_cell_id: Mutex::new(1), - } - } - - pub(crate) async fn stored_values(&self) -> HashMap { - self.stored_values.lock().await.clone() - } - - pub(crate) async fn replace_stored_values(&self, values: HashMap) { - *self.stored_values.lock().await = values; - } - - pub(super) async fn ensure_started( - &self, - ) -> Result>, std::io::Error> { - let mut process_slot = self.process.lock().await; - let needs_spawn = match process_slot.as_mut() { - Some(process) => !matches!(process.has_exited(), Ok(false)), - None => true, - }; - if needs_spawn { - let node_path = resolve_compatible_node(self.js_repl_node_path.as_deref()) - .await - .map_err(std::io::Error::other)?; - *process_slot = Some(spawn_code_mode_process(&node_path).await?); - } - drop(process_slot); - Ok(self.process.clone().lock_owned().await) - } - - pub(crate) async fn start_turn_worker( - &self, - session: &Arc, - turn: &Arc, - router: Arc, - tracker: SharedTurnDiffTracker, - ) -> Option { - if !turn.features.enabled(Feature::CodeMode) { - return None; - } - let exec = ExecContext { - session: Arc::clone(session), - turn: Arc::clone(turn), - }; - let tool_runtime = - ToolCallRuntime::new(router, Arc::clone(session), Arc::clone(turn), tracker); - let mut process_slot = match self.ensure_started().await { - Ok(process_slot) => process_slot, - Err(err) => { - warn!("failed to start {PUBLIC_TOOL_NAME} worker for turn: {err}"); - return None; - } - }; - let Some(process) = process_slot.as_mut() else { - warn!( - "failed to start {PUBLIC_TOOL_NAME} worker for turn: {PUBLIC_TOOL_NAME} runner failed to start" - ); - return None; - }; - Some(process.worker(exec, tool_runtime)) - } - - pub(crate) async fn allocate_cell_id(&self) -> String { - let mut next_cell_id = self.next_cell_id.lock().await; - let cell_id = *next_cell_id; - *next_cell_id = next_cell_id.saturating_add(1); - cell_id.to_string() - } - - pub(crate) async fn allocate_request_id(&self) -> String { - uuid::Uuid::new_v4().to_string() - } -} diff --git a/codex-rs/core/src/tools/code_mode/wait_description.md b/codex-rs/core/src/tools/code_mode/wait_description.md deleted file mode 100644 index 41b928f514..0000000000 --- a/codex-rs/core/src/tools/code_mode/wait_description.md +++ /dev/null @@ -1,8 +0,0 @@ -- Use `wait` only after `exec` returns `Script running with cell ID ...`. -- `cell_id` identifies the running `exec` cell to resume. -- `yield_time_ms` controls how long to wait for more output before yielding again. If omitted, `wait` uses its default wait timeout. -- `max_tokens` limits how much new output this wait call returns. -- `terminate: true` stops the running cell instead of waiting for more output. -- `wait` returns only the new output since the last yield, or the final completion or termination result for that cell. -- If the cell is still running, `wait` may yield again with the same `cell_id`. -- If the cell has already finished, `wait` returns the completed result and closes the cell. diff --git a/codex-rs/core/src/tools/code_mode/wait_handler.rs b/codex-rs/core/src/tools/code_mode/wait_handler.rs index caaf8c8c44..f319985a88 100644 --- a/codex-rs/core/src/tools/code_mode/wait_handler.rs +++ b/codex-rs/core/src/tools/code_mode/wait_handler.rs @@ -8,13 +8,10 @@ use crate::tools::context::ToolPayload; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; -use super::CodeModeSessionProgress; use super::DEFAULT_WAIT_YIELD_TIME_MS; use super::ExecContext; -use super::PUBLIC_TOOL_NAME; use super::WAIT_TOOL_NAME; -use super::handle_node_message; -use super::protocol::HostToNodeMessage; +use super::handle_runtime_response; pub struct CodeModeWaitHandler; @@ -63,66 +60,21 @@ impl ToolHandler for CodeModeWaitHandler { ToolPayload::Function { arguments } if tool_name == WAIT_TOOL_NAME => { let args: ExecWaitArgs = parse_arguments(&arguments)?; let exec = ExecContext { session, turn }; - let request_id = exec - .session - .services - .code_mode_service - .allocate_request_id() - .await; let started_at = std::time::Instant::now(); - let message = if args.terminate { - HostToNodeMessage::Terminate { - request_id: request_id.clone(), - cell_id: args.cell_id.clone(), - } - } else { - HostToNodeMessage::Poll { - request_id: request_id.clone(), - cell_id: args.cell_id.clone(), - yield_time_ms: args.yield_time_ms, - } - }; - let process_slot = exec + let response = exec .session .services .code_mode_service - .ensure_started() + .wait(codex_code_mode::WaitRequest { + cell_id: args.cell_id, + yield_time_ms: args.yield_time_ms, + terminate: args.terminate, + }) .await - .map_err(|err| FunctionCallError::RespondToModel(err.to_string()))?; - let result = { - let mut process_slot = process_slot; - let Some(process) = process_slot.as_mut() else { - return Err(FunctionCallError::RespondToModel(format!( - "{PUBLIC_TOOL_NAME} runner failed to start" - ))); - }; - if !matches!(process.has_exited(), Ok(false)) { - return Err(FunctionCallError::RespondToModel(format!( - "{PUBLIC_TOOL_NAME} runner failed to start" - ))); - } - let message = process - .send(&request_id, &message) - .await - .map_err(|err| err.to_string()); - let message = match message { - Ok(message) => message, - Err(error) => return Err(FunctionCallError::RespondToModel(error)), - }; - handle_node_message( - &exec, - args.cell_id, - message, - Some(args.max_tokens), - started_at, - ) + .map_err(FunctionCallError::RespondToModel)?; + handle_runtime_response(&exec, response, args.max_tokens, started_at) .await - }; - match result { - Ok(CodeModeSessionProgress::Finished(output)) - | Ok(CodeModeSessionProgress::Yielded { output }) => Ok(output), - Err(error) => Err(FunctionCallError::RespondToModel(error)), - } + .map_err(FunctionCallError::RespondToModel) } _ => Err(FunctionCallError::RespondToModel(format!( "{WAIT_TOOL_NAME} expects JSON arguments" diff --git a/codex-rs/core/src/tools/code_mode/worker.rs b/codex-rs/core/src/tools/code_mode/worker.rs deleted file mode 100644 index 5853f3abe3..0000000000 --- a/codex-rs/core/src/tools/code_mode/worker.rs +++ /dev/null @@ -1,116 +0,0 @@ -use tokio::sync::oneshot; -use tokio_util::sync::CancellationToken; -use tracing::error; -use tracing::warn; - -use codex_protocol::models::FunctionCallOutputPayload; -use codex_protocol::models::ResponseInputItem; - -use super::ExecContext; -use super::PUBLIC_TOOL_NAME; -use super::call_nested_tool; -use super::process::CodeModeProcess; -use super::process::write_message; -use super::protocol::HostToNodeMessage; -use super::protocol::NodeToHostMessage; -use crate::tools::parallel::ToolCallRuntime; - -pub(crate) struct CodeModeWorker { - shutdown_tx: Option>, -} - -impl Drop for CodeModeWorker { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -impl CodeModeProcess { - pub(super) fn worker( - &self, - exec: ExecContext, - tool_runtime: ToolCallRuntime, - ) -> CodeModeWorker { - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let stdin = self.stdin.clone(); - let message_rx = self.message_rx.clone(); - tokio::spawn(async move { - loop { - let next_message = tokio::select! { - _ = &mut shutdown_rx => break, - message = async { - let mut message_rx = message_rx.lock().await; - message_rx.recv().await - } => message, - }; - let Some(next_message) = next_message else { - break; - }; - match next_message { - NodeToHostMessage::ToolCall { tool_call } => { - let exec = exec.clone(); - let tool_runtime = tool_runtime.clone(); - let stdin = stdin.clone(); - tokio::spawn(async move { - let result = call_nested_tool( - exec, - tool_runtime, - tool_call.name, - tool_call.input, - CancellationToken::new(), - ) - .await; - let (code_mode_result, error_text) = match result { - Ok(code_mode_result) => (code_mode_result, None), - Err(error) => (serde_json::Value::Null, Some(error.to_string())), - }; - let response = HostToNodeMessage::Response { - request_id: tool_call.request_id, - id: tool_call.id, - code_mode_result, - error_text, - }; - if let Err(err) = write_message(&stdin, &response).await { - warn!("failed to write {PUBLIC_TOOL_NAME} tool response: {err}"); - } - }); - } - NodeToHostMessage::Notify { notify } => { - if notify.text.trim().is_empty() { - continue; - } - if exec - .session - .inject_response_items(vec![ResponseInputItem::CustomToolCallOutput { - call_id: notify.call_id.clone(), - name: Some(PUBLIC_TOOL_NAME.to_string()), - output: FunctionCallOutputPayload::from_text(notify.text), - }]) - .await - .is_err() - { - warn!( - "failed to inject {PUBLIC_TOOL_NAME} notify message for cell {}: no active turn", - notify.cell_id - ); - } - } - unexpected_message @ (NodeToHostMessage::Yielded { .. } - | NodeToHostMessage::Terminated { .. } - | NodeToHostMessage::Result { .. }) => { - error!( - "received unexpected {PUBLIC_TOOL_NAME} message in worker loop: {unexpected_message:?}" - ); - break; - } - } - } - }); - - CodeModeWorker { - shutdown_tx: Some(shutdown_tx), - } - } -} diff --git a/codex-rs/core/src/tools/code_mode_description.rs b/codex-rs/core/src/tools/code_mode_description.rs index b7722aeb72..fb4fc1f519 100644 --- a/codex-rs/core/src/tools/code_mode_description.rs +++ b/codex-rs/core/src/tools/code_mode_description.rs @@ -1,30 +1,11 @@ use crate::client_common::tools::ToolSpec; -use crate::mcp::split_qualified_tool_name; -use crate::tools::code_mode::PUBLIC_TOOL_NAME; -use serde_json::Value as JsonValue; -pub(crate) struct CodeModeToolReference { - pub(crate) module_path: String, - pub(crate) namespace: Vec, - pub(crate) tool_key: String, -} - -pub(crate) fn code_mode_tool_reference(tool_name: &str) -> CodeModeToolReference { - if let Some((server_name, tool_key)) = split_qualified_tool_name(tool_name) { - let namespace = vec!["mcp".to_string(), server_name]; - return CodeModeToolReference { - module_path: format!("tools/{}.js", namespace.join("/")), - namespace, - tool_key, - }; - } - - CodeModeToolReference { - module_path: "tools.js".to_string(), - namespace: Vec::new(), - tool_key: tool_name.to_string(), - } -} +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use codex_code_mode::append_code_mode_sample; +#[allow(unused_imports)] +#[cfg(test)] +pub(crate) use codex_code_mode::render_json_schema_to_typescript; pub(crate) fn augment_tool_spec_for_code_mode(spec: ToolSpec, code_mode_enabled: bool) -> ToolSpec { if !code_mode_enabled { @@ -33,27 +14,27 @@ pub(crate) fn augment_tool_spec_for_code_mode(spec: ToolSpec, code_mode_enabled: match spec { ToolSpec::Function(mut tool) => { - if tool.name != PUBLIC_TOOL_NAME { - tool.description = append_code_mode_sample( - &tool.description, - &tool.name, - "args", - serde_json::to_value(&tool.parameters) - .ok() - .as_ref() - .map(render_json_schema_to_typescript) - .unwrap_or_else(|| "unknown".to_string()), - tool.output_schema - .as_ref() - .map(render_json_schema_to_typescript) - .unwrap_or_else(|| "unknown".to_string()), - ); - } + let input_type = serde_json::to_value(&tool.parameters) + .ok() + .map(|schema| codex_code_mode::render_json_schema_to_typescript(&schema)) + .unwrap_or_else(|| "unknown".to_string()); + let output_type = tool + .output_schema + .as_ref() + .map(codex_code_mode::render_json_schema_to_typescript) + .unwrap_or_else(|| "unknown".to_string()); + tool.description = codex_code_mode::append_code_mode_sample( + &tool.description, + &tool.name, + "args", + input_type, + output_type, + ); ToolSpec::Function(tool) } ToolSpec::Freeform(mut tool) => { - if tool.name != PUBLIC_TOOL_NAME { - tool.description = append_code_mode_sample( + if tool.name != codex_code_mode::PUBLIC_TOOL_NAME { + tool.description = codex_code_mode::append_code_mode_sample( &tool.description, &tool.name, "input", @@ -66,234 +47,3 @@ pub(crate) fn augment_tool_spec_for_code_mode(spec: ToolSpec, code_mode_enabled: other => other, } } - -fn append_code_mode_sample( - description: &str, - tool_name: &str, - input_name: &str, - input_type: String, - output_type: String, -) -> String { - let declaration = format!( - "declare const tools: {{ {} }};", - render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type) - ); - format!("{description}\n\nexec tool declaration:\n```ts\n{declaration}\n```") -} - -fn render_code_mode_tool_declaration( - tool_name: &str, - input_name: &str, - input_type: String, - output_type: String, -) -> String { - let tool_name = normalize_code_mode_identifier(tool_name); - format!("{tool_name}({input_name}: {input_type}): Promise<{output_type}>;") -} - -pub(crate) fn normalize_code_mode_identifier(tool_key: &str) -> String { - let mut identifier = String::new(); - - for (index, ch) in tool_key.chars().enumerate() { - let is_valid = if index == 0 { - ch == '_' || ch == '$' || ch.is_ascii_alphabetic() - } else { - ch == '_' || ch == '$' || ch.is_ascii_alphanumeric() - }; - - if is_valid { - identifier.push(ch); - } else { - identifier.push('_'); - } - } - - if identifier.is_empty() { - "_".to_string() - } else { - identifier - } -} - -fn render_json_schema_to_typescript(schema: &JsonValue) -> String { - render_json_schema_to_typescript_inner(schema) -} - -fn render_json_schema_to_typescript_inner(schema: &JsonValue) -> String { - match schema { - JsonValue::Bool(true) => "unknown".to_string(), - JsonValue::Bool(false) => "never".to_string(), - JsonValue::Object(map) => { - if let Some(value) = map.get("const") { - return render_json_schema_literal(value); - } - - if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array) { - let rendered = values - .iter() - .map(render_json_schema_literal) - .collect::>(); - if !rendered.is_empty() { - return rendered.join(" | "); - } - } - - for key in ["anyOf", "oneOf"] { - if let Some(variants) = map.get(key).and_then(serde_json::Value::as_array) { - let rendered = variants - .iter() - .map(render_json_schema_to_typescript_inner) - .collect::>(); - if !rendered.is_empty() { - return rendered.join(" | "); - } - } - } - - if let Some(variants) = map.get("allOf").and_then(serde_json::Value::as_array) { - let rendered = variants - .iter() - .map(render_json_schema_to_typescript_inner) - .collect::>(); - if !rendered.is_empty() { - return rendered.join(" & "); - } - } - - if let Some(schema_type) = map.get("type") { - if let Some(types) = schema_type.as_array() { - let rendered = types - .iter() - .filter_map(serde_json::Value::as_str) - .map(|schema_type| render_json_schema_type_keyword(map, schema_type)) - .collect::>(); - if !rendered.is_empty() { - return rendered.join(" | "); - } - } - - if let Some(schema_type) = schema_type.as_str() { - return render_json_schema_type_keyword(map, schema_type); - } - } - - if map.contains_key("properties") - || map.contains_key("additionalProperties") - || map.contains_key("required") - { - return render_json_schema_object(map); - } - - if map.contains_key("items") || map.contains_key("prefixItems") { - return render_json_schema_array(map); - } - - "unknown".to_string() - } - _ => "unknown".to_string(), - } -} - -fn render_json_schema_type_keyword( - map: &serde_json::Map, - schema_type: &str, -) -> String { - match schema_type { - "string" => "string".to_string(), - "number" | "integer" => "number".to_string(), - "boolean" => "boolean".to_string(), - "null" => "null".to_string(), - "array" => render_json_schema_array(map), - "object" => render_json_schema_object(map), - _ => "unknown".to_string(), - } -} - -fn render_json_schema_array(map: &serde_json::Map) -> String { - if let Some(items) = map.get("items") { - let item_type = render_json_schema_to_typescript_inner(items); - return format!("Array<{item_type}>"); - } - - if let Some(items) = map.get("prefixItems").and_then(serde_json::Value::as_array) { - let item_types = items - .iter() - .map(render_json_schema_to_typescript_inner) - .collect::>(); - if !item_types.is_empty() { - return format!("[{}]", item_types.join(", ")); - } - } - - "unknown[]".to_string() -} - -fn render_json_schema_object(map: &serde_json::Map) -> String { - let required = map - .get("required") - .and_then(serde_json::Value::as_array) - .map(|items| { - items - .iter() - .filter_map(serde_json::Value::as_str) - .collect::>() - }) - .unwrap_or_default(); - let properties = map - .get("properties") - .and_then(serde_json::Value::as_object) - .cloned() - .unwrap_or_default(); - - let mut sorted_properties = properties.iter().collect::>(); - sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); - let mut lines = sorted_properties - .into_iter() - .map(|(name, value)| { - let optional = if required.iter().any(|required_name| required_name == name) { - "" - } else { - "?" - }; - let property_name = render_json_schema_property_name(name); - let property_type = render_json_schema_to_typescript_inner(value); - format!("{property_name}{optional}: {property_type};") - }) - .collect::>(); - - if let Some(additional_properties) = map.get("additionalProperties") { - let additional_type = match additional_properties { - JsonValue::Bool(true) => Some("unknown".to_string()), - JsonValue::Bool(false) => None, - value => Some(render_json_schema_to_typescript_inner(value)), - }; - - if let Some(additional_type) = additional_type { - lines.push(format!("[key: string]: {additional_type};")); - } - } else if properties.is_empty() { - lines.push("[key: string]: unknown;".to_string()); - } - - if lines.is_empty() { - return "{}".to_string(); - } - - format!("{{ {} }}", lines.join(" ")) -} - -fn render_json_schema_property_name(name: &str) -> String { - if normalize_code_mode_identifier(name) == name { - name.to_string() - } else { - serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\""))) - } -} - -fn render_json_schema_literal(value: &JsonValue) -> String { - serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string()) -} - -#[cfg(test)] -#[path = "code_mode_description_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index 8544eb404a..345f7ce06f 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -4,7 +4,6 @@ use crate::codex::TurnContext; use crate::function_tool::FunctionCallError; use crate::mcp_connection_manager::ToolInfo; use crate::sandboxing::SandboxPermissions; -use crate::tools::code_mode::is_code_mode_nested_tool; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; @@ -67,7 +66,7 @@ impl ToolRouter { specs .iter() .filter_map(|configured_tool| { - if !is_code_mode_nested_tool(configured_tool.spec.name()) { + if !codex_code_mode::is_code_mode_nested_tool(configured_tool.spec.name()) { Some(configured_tool.spec.clone()) } else { None diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 18afb20bc5..6ff8349b53 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -33,7 +33,6 @@ use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::sandbox_override_for_first_attempt; use crate::tools::sandboxing::with_cached_approval; -use codex_features::Feature; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; @@ -227,9 +226,7 @@ impl ToolRuntime for ShellRuntime { &req.cwd, &req.explicit_env_overrides, ); - let command = if matches!(session_shell.shell_type, ShellType::PowerShell) - && ctx.session.features().enabled(Feature::PowershellUtf8) - { + let command = if matches!(session_shell.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) } else { command diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 0b64092156..49ef0e6385 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -36,7 +36,6 @@ use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; -use codex_features::Feature; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; @@ -200,9 +199,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &req.cwd, &req.explicit_env_overrides, ); - let command = if matches!(session_shell.shell_type, ShellType::PowerShell) - && ctx.session.features().enabled(Feature::PowershellUtf8) - { + let command = if matches!(session_shell.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) } else { command diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 5ae2f333df..2e0a413a6f 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -11,9 +11,6 @@ use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::code_mode::PUBLIC_TOOL_NAME; use crate::tools::code_mode::WAIT_TOOL_NAME; -use crate::tools::code_mode::is_code_mode_nested_tool; -use crate::tools::code_mode::tool_description as code_mode_tool_description; -use crate::tools::code_mode::wait_tool_description as code_mode_wait_tool_description; use crate::tools::code_mode_description::augment_tool_spec_for_code_mode; use crate::tools::discoverable::DiscoverablePluginInfo; use crate::tools::discoverable::DiscoverableTool; @@ -833,7 +830,7 @@ fn create_wait_tool() -> ToolSpec { name: WAIT_TOOL_NAME.to_string(), description: format!( "Waits on a yielded `{PUBLIC_TOOL_NAME}` cell and returns new output or completion.\n{}", - code_mode_wait_tool_description().trim() + codex_code_mode::build_wait_tool_description().trim() ), strict: false, parameters: JsonSchema::Object { @@ -2176,7 +2173,10 @@ SOURCE: /[\s\S]+/ ToolSpec::Freeform(FreeformTool { name: PUBLIC_TOOL_NAME.to_string(), - description: code_mode_tool_description(enabled_tools, code_mode_only_enabled), + description: codex_code_mode::build_exec_tool_description( + enabled_tools, + code_mode_only_enabled, + ), format: FreeformToolFormat { r#type: "grammar".to_string(), syntax: "lark".to_string(), @@ -2647,7 +2647,7 @@ pub(crate) fn build_specs_with_discoverable_tools( ToolSpec::Freeform(tool) => (tool.name, tool.description), _ => return None, }; - is_code_mode_nested_tool(&name).then_some((name, description)) + codex_code_mode::is_code_mode_nested_tool(&name).then_some((name, description)) }) .collect::>(); enabled_tools.sort_by(|left, right| left.0.cmp(&right.0)); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index c74de38e86..b9e4f05b36 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -1672,8 +1672,6 @@ async fn code_mode_exit_stops_script_immediately() -> Result<()> { &server, "use exec to stop script early with exit helper", r#" -import { exit, text } from "@openai/code_mode"; - text("before"); exit(); text("after"); @@ -2129,6 +2127,7 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "SuppressedError", "Symbol", "SyntaxError", + "Temporal", "TypeError", "URIError", "Uint16Array", @@ -2141,7 +2140,6 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "WebAssembly", "__codexContentItems", "add_content", - "console", "decodeURI", "decodeURIComponent", "encodeURI", @@ -2282,10 +2280,8 @@ async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> { test.session_configured = new_thread.session_configured; let code = r#" -import { ALL_TOOLS, hidden_dynamic_tool } from "tools.js"; - const tool = ALL_TOOLS.find(({ name }) => name === "hidden_dynamic_tool"); -const out = await hidden_dynamic_tool({ city: "Paris" }); +const out = await tools.hidden_dynamic_tool({ city: "Paris" }); text( JSON.stringify({ name: tool?.name ?? null, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index cdf69acdc0..2838233e7f 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -493,6 +493,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let expected = DeveloperInstructions::from_policy( &sandbox_policy, AskForApproval::OnRequest, + test.config.approvals_reviewer, &Policy::empty(), test.config.cwd.as_path(), false, diff --git a/codex-rs/core/tests/suite/shell_command.rs b/codex-rs/core/tests/suite/shell_command.rs index 9a128b6a80..6eda4a7c36 100644 --- a/codex-rs/core/tests/suite/shell_command.rs +++ b/codex-rs/core/tests/suite/shell_command.rs @@ -1,7 +1,6 @@ use std::time::Duration; use anyhow::Result; -use codex_features::Feature; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -251,16 +250,7 @@ async fn shell_command_times_out_with_timeout_ms() -> anyhow::Result<()> { async fn unicode_output(login: bool) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); - #[allow(clippy::expect_used)] - let harness = shell_command_harness_with(|builder| { - builder.with_model("gpt-5.2").with_config(|config| { - config - .features - .enable(Feature::PowershellUtf8) - .expect("test config should allow feature update"); - }) - }) - .await?; + let harness = shell_command_harness_with(|builder| builder.with_model("gpt-5.2")).await?; // We use a child process on windows instead of a direct builtin like 'echo' to ensure that Powershell // config is actually being set correctly. @@ -286,16 +276,7 @@ async fn unicode_output(login: bool) -> anyhow::Result<()> { async fn unicode_output_with_newlines(login: bool) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); - #[allow(clippy::expect_used)] - let harness = shell_command_harness_with(|builder| { - builder.with_model("gpt-5.2").with_config(|config| { - config - .features - .enable(Feature::PowershellUtf8) - .expect("test config should allow feature update"); - }) - }) - .await?; + let harness = shell_command_harness_with(|builder| builder.with_model("gpt-5.2")).await?; let call_id = "unicode_output"; mount_shell_responses_with_timeout( diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 1e6073be09..7252d9a6b6 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -159,7 +159,9 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { let call_id = "uexec-apply-patch"; let args = json!({ "cmd": command, - "yield_time_ms": 250, + // The intercepted apply_patch path spawns a helper process, which can + // take longer than a tiny unified-exec yield deadline on CI. + "yield_time_ms": 5_000, }); let responses = vec![ diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index 6a3b947988..bad1c8f92e 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -73,12 +73,6 @@ ignore = [ { id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" }, { id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map/starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" }, { id = "RUSTSEC-2024-0436", reason = "paste is unmaintained; pulled in via ratatui/rmcp/starlark used by tui/execpolicy; no fixed release yet" }, - # TODO(fcoury): remove these exceptions when the aws-lc-sys upgrade path is Bazel-compatible in this workspace. - { id = "RUSTSEC-2026-0044", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" }, - { id = "RUSTSEC-2026-0045", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" }, - { id = "RUSTSEC-2026-0046", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" }, - { id = "RUSTSEC-2026-0047", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" }, - { id = "RUSTSEC-2026-0048", reason = "aws-lc-sys remains on 0.37.0 because upgrading currently breaks Bazel fetch/build for this workspace" }, # TODO(fcoury): remove this exception when syntect drops yaml-rust and bincode, or updates to versions that have fixed the vulnerabilities. { 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" }, diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 101cbda734..f49428cec5 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -132,8 +132,6 @@ pub enum Feature { ChildAgentsMd, /// Allow the model to request `detail: "original"` image outputs on supported models. ImageDetailOriginal, - /// Enforce UTF8 output in Powershell. - PowershellUtf8, /// Compress request bodies (zstd) when sending streaming requests to codex-backend. EnableRequestCompression, /// Enable collab tools. @@ -689,18 +687,6 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Removed, default_enabled: false, }, - FeatureSpec { - id: Feature::PowershellUtf8, - key: "powershell_utf8", - #[cfg(windows)] - stage: Stage::Stable, - #[cfg(windows)] - default_enabled: true, - #[cfg(not(windows))] - stage: Stage::UnderDevelopment, - #[cfg(not(windows))] - default_enabled: false, - }, FeatureSpec { id: Feature::EnableRequestCompression, key: "enable_request_compression", diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 1368a93f61..5c68d3c3e3 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -9,6 +9,7 @@ use serde::Serialize; use serde::ser::Serializer; use ts_rs::TS; +use crate::config_types::ApprovalsReviewer; use crate::config_types::CollaborationMode; use crate::config_types::SandboxMode; use crate::protocol::AskForApproval; @@ -478,9 +479,10 @@ const APPROVAL_POLICY_UNLESS_TRUSTED: &str = const APPROVAL_POLICY_ON_FAILURE: &str = include_str!("prompts/permissions/approval_policy/on_failure.md"); const APPROVAL_POLICY_ON_REQUEST_RULE: &str = - include_str!("prompts/permissions/approval_policy/on_request_rule.md"); + include_str!("prompts/permissions/approval_policy/on_request.md"); const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str = include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md"); +const GUARDIAN_SUBAGENT_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `guardian_subagent`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval."; const SANDBOX_MODE_DANGER_FULL_ACCESS: &str = include_str!("prompts/permissions/sandbox_mode/danger_full_access.md"); @@ -491,6 +493,14 @@ const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_m const REALTIME_START_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_start.md"); const REALTIME_END_INSTRUCTIONS: &str = include_str!("prompts/realtime/realtime_end.md"); +struct PermissionsPromptConfig<'a> { + approval_policy: AskForApproval, + approvals_reviewer: ApprovalsReviewer, + exec_policy: &'a Policy, + exec_permission_approvals_enabled: bool, + request_permissions_tool_enabled: bool, +} + impl DeveloperInstructions { pub fn new>(text: T) -> Self { Self { text: text.into() } @@ -498,6 +508,7 @@ impl DeveloperInstructions { pub fn from( approval_policy: AskForApproval, + approvals_reviewer: ApprovalsReviewer, exec_policy: &Policy, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, @@ -541,6 +552,14 @@ impl DeveloperInstructions { ), }; + let text = if approvals_reviewer == ApprovalsReviewer::GuardianSubagent + && approval_policy != AskForApproval::Never + { + format!("{text}\n\n{GUARDIAN_SUBAGENT_APPROVAL_SUFFIX}") + } else { + text + }; + DeveloperInstructions::new(text) } @@ -590,6 +609,7 @@ impl DeveloperInstructions { pub fn from_policy( sandbox_policy: &SandboxPolicy, approval_policy: AskForApproval, + approvals_reviewer: ApprovalsReviewer, exec_policy: &Policy, cwd: &Path, exec_permission_approvals_enabled: bool, @@ -614,11 +634,14 @@ impl DeveloperInstructions { DeveloperInstructions::from_permissions_with_network( sandbox_mode, network_access, - approval_policy, - exec_policy, + PermissionsPromptConfig { + approval_policy, + approvals_reviewer, + exec_policy, + exec_permission_approvals_enabled, + request_permissions_tool_enabled, + }, writable_roots, - exec_permission_approvals_enabled, - request_permissions_tool_enabled, ) } @@ -639,11 +662,8 @@ impl DeveloperInstructions { fn from_permissions_with_network( sandbox_mode: SandboxMode, network_access: NetworkAccess, - approval_policy: AskForApproval, - exec_policy: &Policy, + config: PermissionsPromptConfig<'_>, writable_roots: Option>, - exec_permission_approvals_enabled: bool, - request_permissions_tool_enabled: bool, ) -> Self { let start_tag = DeveloperInstructions::new(""); let end_tag = DeveloperInstructions::new(""); @@ -653,10 +673,11 @@ impl DeveloperInstructions { network_access, )) .concat(DeveloperInstructions::from( - approval_policy, - exec_policy, - exec_permission_approvals_enabled, - request_permissions_tool_enabled, + config.approval_policy, + config.approvals_reviewer, + config.exec_policy, + config.exec_permission_approvals_enabled, + config.request_permissions_tool_enabled, )) .concat(DeveloperInstructions::from_writable_roots(writable_roots)) .concat(end_tag) @@ -1923,11 +1944,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnRequest, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: false, + request_permissions_tool_enabled: false, + }, None, - false, - false, ); let text = instructions.into_text(); @@ -1954,6 +1978,7 @@ mod tests { let instructions = DeveloperInstructions::from_policy( &policy, AskForApproval::UnlessTrusted, + ApprovalsReviewer::User, &Policy::empty(), &PathBuf::from("/tmp"), false, @@ -1976,11 +2001,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnRequest, - &exec_policy, + PermissionsPromptConfig { + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &exec_policy, + exec_permission_approvals_enabled: false, + request_permissions_tool_enabled: false, + }, None, - false, - false, ); let text = instructions.into_text(); @@ -1994,11 +2022,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::UnlessTrusted, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::UnlessTrusted, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: false, + request_permissions_tool_enabled: true, + }, None, - false, - true, ); let text = instructions.into_text(); @@ -2011,11 +2042,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnFailure, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::OnFailure, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: false, + request_permissions_tool_enabled: true, + }, None, - false, - true, ); let text = instructions.into_text(); @@ -2028,11 +2062,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnRequest, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: true, + request_permissions_tool_enabled: false, + }, None, - true, - false, ); let text = instructions.into_text(); @@ -2045,11 +2082,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnRequest, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: false, + request_permissions_tool_enabled: true, + }, None, - false, - true, ); let text = instructions.into_text(); @@ -2064,11 +2104,14 @@ mod tests { let instructions = DeveloperInstructions::from_permissions_with_network( SandboxMode::WorkspaceWrite, NetworkAccess::Enabled, - AskForApproval::OnRequest, - &Policy::empty(), + PermissionsPromptConfig { + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + exec_policy: &Policy::empty(), + exec_permission_approvals_enabled: true, + request_permissions_tool_enabled: true, + }, None, - true, - true, ); let text = instructions.into_text(); @@ -2076,6 +2119,35 @@ mod tests { assert!(text.contains("# request_permissions Tool")); } + #[test] + fn guardian_subagent_approvals_append_guardian_specific_guidance() { + let text = DeveloperInstructions::from( + AskForApproval::OnRequest, + ApprovalsReviewer::GuardianSubagent, + &Policy::empty(), + false, + false, + ) + .into_text(); + + assert!(text.contains("`approvals_reviewer` is `guardian_subagent`")); + assert!(text.contains("materially safer alternative")); + } + + #[test] + fn guardian_subagent_approvals_omit_guardian_specific_guidance_when_approval_is_never() { + let text = DeveloperInstructions::from( + AskForApproval::Never, + ApprovalsReviewer::GuardianSubagent, + &Policy::empty(), + false, + false, + ) + .into_text(); + + assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`")); + } + fn granular_categories_section(title: &str, categories: &[&str]) -> String { format!("{title}\n{}", categories.join("\n")) } @@ -2118,6 +2190,7 @@ mod tests { request_permissions: true, mcp_elicitations: false, }), + ApprovalsReviewer::User, &Policy::empty(), true, false, @@ -2151,6 +2224,7 @@ mod tests { request_permissions: true, mcp_elicitations: true, }), + ApprovalsReviewer::User, &Policy::empty(), true, false, @@ -2183,6 +2257,7 @@ mod tests { request_permissions: true, mcp_elicitations: true, }), + ApprovalsReviewer::User, &Policy::empty(), false, false, @@ -2215,6 +2290,7 @@ mod tests { request_permissions: true, mcp_elicitations: true, }), + ApprovalsReviewer::User, &Policy::empty(), true, true, @@ -2230,6 +2306,7 @@ mod tests { request_permissions: false, mcp_elicitations: true, }), + ApprovalsReviewer::User, &Policy::empty(), true, true, @@ -2249,6 +2326,7 @@ mod tests { request_permissions: true, mcp_elicitations: false, }), + ApprovalsReviewer::User, &Policy::empty(), true, false, diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 590fbd1fe1..0a5b337029 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -347,6 +347,48 @@ impl FileSystemSandboxPolicy { self.resolve_access_with_cwd(path, cwd).can_write() } + pub fn with_additional_readable_roots( + mut self, + cwd: &Path, + additional_readable_roots: &[AbsolutePathBuf], + ) -> Self { + if self.has_full_disk_read_access() { + return self; + } + + for path in additional_readable_roots { + if self.can_read_path_with_cwd(path.as_path(), cwd) { + continue; + } + + self.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { path: path.clone() }, + access: FileSystemAccessMode::Read, + }); + } + + self + } + + pub fn with_additional_writable_roots( + mut self, + cwd: &Path, + additional_writable_roots: &[AbsolutePathBuf], + ) -> Self { + for path in additional_writable_roots { + if self.can_write_path_with_cwd(path.as_path(), cwd) { + continue; + } + + self.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { path: path.clone() }, + access: FileSystemAccessMode::Write, + }); + } + + self + } + pub fn needs_direct_runtime_enforcement( &self, network_policy: NetworkSandboxPolicy, @@ -1782,6 +1824,74 @@ mod tests { ); } + #[test] + fn with_additional_readable_roots_skips_existing_effective_access() { + let cwd = TempDir::new().expect("tempdir"); + let cwd_root = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd"); + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Read, + }]); + + let actual = policy + .clone() + .with_additional_readable_roots(cwd.path(), std::slice::from_ref(&cwd_root)); + + assert_eq!(actual, policy); + } + + #[test] + fn with_additional_writable_roots_skips_existing_effective_access() { + let cwd = TempDir::new().expect("tempdir"); + let cwd_root = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd"); + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }]); + + let actual = policy + .clone() + .with_additional_writable_roots(cwd.path(), std::slice::from_ref(&cwd_root)); + + assert_eq!(actual, policy); + } + + #[test] + fn with_additional_writable_roots_adds_new_root() { + let temp_dir = TempDir::new().expect("tempdir"); + let cwd = temp_dir.path().join("workspace"); + let extra = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("extra")) + .expect("resolve extra root"); + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }]); + + let actual = policy.with_additional_writable_roots(&cwd, std::slice::from_ref(&extra)); + + assert_eq!( + actual, + FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: extra }, + access: FileSystemAccessMode::Write, + }, + ]) + ); + } + #[test] fn file_system_access_mode_orders_by_conflict_precedence() { assert!(FileSystemAccessMode::Write > FileSystemAccessMode::Read); diff --git a/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request.md similarity index 90% rename from codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md rename to codex-rs/protocol/src/prompts/permissions/approval_policy/on_request.md index 3928a91b7e..092f375c13 100644 --- a/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request_rule.md +++ b/codex-rs/protocol/src/prompts/permissions/approval_policy/on_request.md @@ -19,6 +19,8 @@ This is treated as two command segments: ["tee", "output.txt"] +Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows. + ## How to request escalation IMPORTANT: To request approval to execute a command that will require escalated privileges: @@ -44,7 +46,7 @@ While commands are running inside the sandbox, here are some scenarios that will When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`. ### Banned prefix_rules -Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes. +Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting. NEVER provide a prefix_rule argument for destructive commands like rm. NEVER provide a prefix_rule if your command uses a heredoc or herestring. @@ -52,5 +54,4 @@ NEVER provide a prefix_rule if your command uses a heredoc or herestring. Good examples of prefixes: - ["npm", "run", "dev"] - ["gh", "pr", "check"] -- ["pytest"] - ["cargo", "test"] diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 808be6259e..4f7f2616f7 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2978,6 +2978,14 @@ pub enum Product { Atlas, } impl Product { + pub fn to_app_platform(self) -> &'static str { + match self { + Self::Chatgpt => "chat", + Self::Codex => "codex", + Self::Atlas => "atlas", + } + } + pub fn from_session_source_name(value: &str) -> Option { let normalized = value.trim().to_ascii_lowercase(); match normalized.as_str() { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ee0a7bb636..8116cf972b 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -403,6 +403,7 @@ pub(crate) struct ChatComposer { config: ChatComposerConfig, collaboration_mode_indicator: Option, connectors_enabled: bool, + plugins_command_enabled: bool, fast_command_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, @@ -441,6 +442,7 @@ impl ChatComposer { BuiltinCommandFlags { collaboration_modes_enabled: self.collaboration_modes_enabled, connectors_enabled: self.connectors_enabled, + plugins_command_enabled: self.plugins_command_enabled, fast_command_enabled: self.fast_command_enabled, personality_command_enabled: self.personality_command_enabled, realtime_conversation_enabled: self.realtime_conversation_enabled, @@ -525,6 +527,7 @@ impl ChatComposer { config, collaboration_mode_indicator: None, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, @@ -559,6 +562,10 @@ impl ChatComposer { self.sync_popups(); } + pub fn set_plugins_command_enabled(&mut self, enabled: bool) { + self.plugins_command_enabled = enabled; + } + /// Toggle composer-side image paste handling. /// /// This only affects whether image-like paste content is converted into attachments; the @@ -3477,6 +3484,7 @@ impl ChatComposer { if is_editing_slash_command_name { let collaboration_modes_enabled = self.collaboration_modes_enabled; let connectors_enabled = self.connectors_enabled; + let plugins_command_enabled = self.plugins_command_enabled; let fast_command_enabled = self.fast_command_enabled; let personality_command_enabled = self.personality_command_enabled; let realtime_conversation_enabled = self.realtime_conversation_enabled; @@ -3486,6 +3494,7 @@ impl ChatComposer { CommandPopupFlags { collaboration_modes_enabled, connectors_enabled, + plugins_command_enabled, fast_command_enabled, personality_command_enabled, realtime_conversation_enabled, diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 8508849689..e7269c38ef 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -38,6 +38,7 @@ pub(crate) struct CommandPopup { pub(crate) struct CommandPopupFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) connectors_enabled: bool, + pub(crate) plugins_command_enabled: bool, pub(crate) fast_command_enabled: bool, pub(crate) personality_command_enabled: bool, pub(crate) realtime_conversation_enabled: bool, @@ -50,6 +51,7 @@ impl From for slash_commands::BuiltinCommandFlags { Self { collaboration_modes_enabled: value.collaboration_modes_enabled, connectors_enabled: value.connectors_enabled, + plugins_command_enabled: value.plugins_command_enabled, fast_command_enabled: value.fast_command_enabled, personality_command_enabled: value.personality_command_enabled, realtime_conversation_enabled: value.realtime_conversation_enabled, @@ -509,6 +511,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -531,6 +534,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -553,6 +557,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, @@ -583,6 +588,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -605,6 +611,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: false, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: true, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 56b25dfa1a..0c9e16b411 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -266,6 +266,11 @@ impl BottomPane { self.request_redraw(); } + pub fn set_plugins_command_enabled(&mut self, enabled: bool) { + self.composer.set_plugins_command_enabled(enabled); + self.request_redraw(); + } + pub fn take_mention_bindings(&mut self) -> Vec { self.composer.take_mention_bindings() } diff --git a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs index 315e311e02..1f38a17773 100644 --- a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs +++ b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs @@ -176,6 +176,21 @@ mod tests { assert_snapshot!("render_one_message", format!("{buf:?}")); } + #[test] + fn render_one_message_with_shift_left_binding() { + let mut queue = PendingInputPreview::new(); + queue.queued_messages.push("Hello, world!".to_string()); + queue.set_edit_binding(key_hint::shift(KeyCode::Left)); + let width = 40; + let height = queue.desired_height(width); + let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); + queue.render(Rect::new(0, 0, width, height), &mut buf); + assert_snapshot!( + "render_one_message_with_shift_left_binding", + format!("{buf:?}") + ); + } + #[test] fn render_two_messages() { let mut queue = PendingInputPreview::new(); diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index 15b70f232c..54b1a8cf4e 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -14,6 +14,7 @@ use crate::slash_command::built_in_slash_commands; pub(crate) struct BuiltinCommandFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) connectors_enabled: bool, + pub(crate) plugins_command_enabled: bool, pub(crate) fast_command_enabled: bool, pub(crate) personality_command_enabled: bool, pub(crate) realtime_conversation_enabled: bool, @@ -31,6 +32,7 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st || !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan) }) .filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps) + .filter(|(_, cmd)| flags.plugins_command_enabled || *cmd != SlashCommand::Plugins) .filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast) .filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality) .filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime) @@ -63,6 +65,7 @@ mod tests { BuiltinCommandFlags { collaboration_modes_enabled: true, connectors_enabled: true, + plugins_command_enabled: true, fast_command_enabled: true, personality_command_enabled: true, realtime_conversation_enabled: true, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap new file mode 100644 index 0000000000..f3ad37e731 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap @@ -0,0 +1,21 @@ +--- +source: tui/src/bottom_pane/pending_input_preview.rs +expression: "format!(\"{buf:?}\")" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 40, height: 3 }, + content: [ + "• Queued follow-up messages ", + " ↳ Hello, world! ", + " shift + ← edit last queued message ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC, + x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e2480c4ec0..4a6ef92865 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -155,6 +155,8 @@ use codex_protocol::request_permissions::RequestPermissionsEvent; use codex_protocol::request_user_input::RequestUserInputEvent; use codex_protocol::user_input::TextElement; use codex_protocol::user_input::UserInput; +use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_terminal_detection::terminal_info; use codex_utils_sleep_inhibitor::SleepInhibitor; @@ -195,14 +197,21 @@ const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection"; /// Choose the keybinding used to edit the most-recently queued message. /// /// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently -/// swallow Alt+Up, so users in those environments would never be able to trigger -/// the edit action. We fall back to Shift+Left for those terminals while -/// keeping the more discoverable Alt+Up everywhere else. +/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall +/// back to Shift+Left for those environments while keeping the more discoverable +/// Alt+Up everywhere else. /// /// The match is exhaustive so that adding a new `TerminalName` variant forces /// an explicit decision about which binding that terminal should use. -fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding { - match terminal_name { +fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding { + if matches!( + terminal_info.multiplexer.as_ref(), + Some(Multiplexer::Tmux { .. }) + ) { + return key_hint::shift(KeyCode::Left); + } + + match terminal_info.name { TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => { key_hint::shift(KeyCode::Left) } @@ -720,6 +729,7 @@ pub(crate) struct ChatWidget { connectors_partial_snapshot: Option, connectors_prefetch_in_flight: bool, connectors_force_refetch_pending: bool, + pending_mcp_output_requests: usize, plugins_cache: PluginsCacheState, plugins_fetch_state: PluginListFetchState, // Queue of interruptive UI events deferred during an active write cycle @@ -1430,6 +1440,7 @@ impl ChatWidget { self.refresh_model_display(); self.sync_fast_command_enabled(); self.sync_personality_command_enabled(); + self.sync_plugins_command_enabled(); self.refresh_plugin_mentions(); let startup_tooltip_override = self.startup_tooltip_override.take(); let show_fast_status = self.should_show_fast_status(&model_for_header, event.service_tier); @@ -1455,9 +1466,6 @@ impl ChatWidget { cwds: Vec::new(), force_reload: true, }); - if self.connectors_enabled() { - self.prefetch_connectors(); - } if let Some(user_message) = self.initial_user_message.take() { self.submit_user_message(user_message); } @@ -2183,6 +2191,7 @@ impl ChatWidget { } fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) { + let codex_apps_ready = ev.ready.iter().any(|server| server == "codex_apps"); let mut parts = Vec::new(); if !ev.failed.is_empty() { let failed_servers: Vec<_> = ev.failed.iter().map(|f| f.server.clone()).collect(); @@ -2201,6 +2210,11 @@ impl ChatWidget { self.mcp_startup_status = None; self.update_task_running_state(); self.maybe_send_next_queued_input(); + if self.connectors_enabled() && codex_apps_ready { + // Populate `$` app mentions from the session's already-started MCP manager + // instead of doing a separate TUI-side connector prefetch. + self.submit_op(Op::ListMcpTools); + } self.request_redraw(); } @@ -3612,8 +3626,7 @@ impl ChatWidget { let active_cell = Some(Self::placeholder_session_header_cell(&config)); let current_cwd = Some(config.cwd.clone()); - let queued_message_edit_binding = - queued_message_edit_binding_for_terminal(terminal_info().name); + let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), @@ -3664,6 +3677,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -3738,6 +3752,7 @@ impl ChatWidget { .set_collaboration_modes_enabled(/*enabled*/ true); widget.sync_fast_command_enabled(); widget.sync_personality_command_enabled(); + widget.sync_plugins_command_enabled(); widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_binding); @@ -3812,8 +3827,7 @@ impl ChatWidget { let active_cell = Some(Self::placeholder_session_header_cell(&config)); let current_cwd = Some(config.cwd.clone()); - let queued_message_edit_binding = - queued_message_edit_binding_for_terminal(terminal_info().name); + let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), @@ -3864,6 +3878,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -3938,6 +3953,7 @@ impl ChatWidget { .set_collaboration_modes_enabled(/*enabled*/ true); widget.sync_fast_command_enabled(); widget.sync_personality_command_enabled(); + widget.sync_plugins_command_enabled(); widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_binding); @@ -4004,8 +4020,7 @@ impl ChatWidget { settings: fallback_default, }; - let queued_message_edit_binding = - queued_message_edit_binding_for_terminal(terminal_info().name); + let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), @@ -4056,6 +4071,7 @@ impl ChatWidget { connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -4130,6 +4146,7 @@ impl ChatWidget { .set_collaboration_modes_enabled(/*enabled*/ true); widget.sync_fast_command_enabled(); widget.sync_personality_command_enabled(); + widget.sync_plugins_command_enabled(); widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_binding); @@ -5939,10 +5956,6 @@ impl ChatWidget { self.prefetch_connectors_with_options(force_refetch); } - fn prefetch_connectors(&mut self) { - self.prefetch_connectors_with_options(/*force_refetch*/ false); - } - fn prefetch_connectors_with_options(&mut self, force_refetch: bool) { if !self.connectors_enabled() { return; @@ -7823,6 +7836,7 @@ impl ChatWidget { self.sync_personality_command_enabled(); } if feature == Feature::Plugins { + self.sync_plugins_command_enabled(); self.refresh_plugin_mentions(); } if feature == Feature::PreventIdleSleep { @@ -8025,6 +8039,11 @@ impl ChatWidget { .set_personality_command_enabled(self.config.features.enabled(Feature::Personality)); } + fn sync_plugins_command_enabled(&mut self) { + self.bottom_pane + .set_plugins_command_enabled(self.config.features.enabled(Feature::Plugins)); + } + fn current_model_supports_personality(&self) -> bool { let model = self.current_model(); self.models_manager @@ -8358,8 +8377,8 @@ impl ChatWidget { .is_empty() { self.add_to_history(history_cell::empty_mcp_output()); - } else { - self.submit_op(Op::ListMcpTools); + } else if self.submit_op(Op::ListMcpTools) { + self.pending_mcp_output_requests = self.pending_mcp_output_requests.saturating_add(1); } } @@ -8821,13 +8840,77 @@ impl ChatWidget { } fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) { - self.add_to_history(history_cell::new_mcp_tools_output( - &self.config, - ev.tools, - ev.resources, - ev.resource_templates, - &ev.auth_statuses, - )); + if self.connectors_enabled() { + let mut connectors_by_id: HashMap = HashMap::new(); + for tool in ev.tools.values() { + let Some(meta) = tool.meta.as_ref().and_then(serde_json::Value::as_object) else { + continue; + }; + let Some(connector_id) = meta + .get("connector_id") + .and_then(serde_json::Value::as_str) + .filter(|id| !id.is_empty()) + else { + continue; + }; + connectors_by_id + .entry(connector_id.to_string()) + .or_insert_with(|| { + let name = meta + .get("connector_name") + .or_else(|| meta.get("connector_display_name")) + .and_then(serde_json::Value::as_str) + .filter(|name| !name.trim().is_empty()) + .unwrap_or(connector_id) + .to_string(); + let description = meta + .get("connector_description") + .or_else(|| meta.get("connectorDescription")) + .and_then(serde_json::Value::as_str) + .map(str::trim) + .filter(|description| !description.is_empty()) + .map(ToString::to_string); + connectors::AppInfo { + id: connector_id.to_string(), + name, + description, + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: None, + is_accessible: true, + is_enabled: true, + plugin_display_names: Vec::new(), + } + }); + } + + let mut app_connectors = connectors_by_id.into_values().collect::>(); + app_connectors.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then_with(|| left.id.cmp(&right.id)) + }); + let app_connectors = connectors::with_app_enabled_state(app_connectors, &self.config); + self.bottom_pane + .set_connectors_snapshot(Some(ConnectorsSnapshot { + connectors: app_connectors, + })); + } + + if self.pending_mcp_output_requests > 0 { + self.pending_mcp_output_requests -= 1; + self.add_to_history(history_cell::new_mcp_tools_output( + &self.config, + ev.tools, + ev.resources, + ev.resource_templates, + &ev.auth_statuses, + )); + } } fn on_list_custom_prompts(&mut self, ev: ListCustomPromptsResponseEvent) { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a614d1361d..ed37fc5e72 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -120,6 +120,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; use codex_protocol::user_input::TextElement; use codex_protocol::user_input::UserInput; +use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_approval_presets::builtin_approval_presets; @@ -1895,6 +1897,7 @@ async fn make_chatwidget_manual( connectors_partial_snapshot: None, connectors_prefetch_in_flight: false, connectors_force_refetch_pending: false, + pending_mcp_output_requests: 0, plugins_cache: PluginsCacheState::default(), plugins_fetch_state: PluginListFetchState::default(), interrupts: InterruptManager::new(), @@ -3755,10 +3758,10 @@ async fn alt_up_edits_most_recent_queued_message() { } async fn assert_shift_left_edits_most_recent_queued_message_for_terminal( - terminal_name: TerminalName, + terminal_info: TerminalInfo, ) { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name); + chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info); chat.bottom_pane .set_queued_message_edit_binding(chat.queued_message_edit_binding); @@ -3790,37 +3793,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal( #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal) - .await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::AppleTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; } #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal) - .await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::WarpTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; } #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::VsCode, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; +} + +#[tokio::test] +async fn shift_left_edits_most_recent_queued_message_in_tmux() { + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: Some(Multiplexer::Tmux { version: None }), + }) + .await; } #[test] -fn queued_message_edit_binding_mapping_covers_special_terminals() { +fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() { assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::AppleTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::WarpTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::VsCode), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::VsCode, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::Iterm2), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: Some(Multiplexer::Tmux { version: None }), + }), + crate::key_hint::shift(KeyCode::Left) + ); + assert_eq!( + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::alt(KeyCode::Up) ); } diff --git a/codex-rs/tui_app_server/src/app.rs b/codex-rs/tui_app_server/src/app.rs index 171df69270..d4569ae7a8 100644 --- a/codex-rs/tui_app_server/src/app.rs +++ b/codex-rs/tui_app_server/src/app.rs @@ -464,8 +464,6 @@ enum ThreadBufferedEvent { Notification(ServerNotification), Request(ServerRequest), HistoryEntryResponse(GetHistoryEntryResponseEvent), - LegacyWarning(String), - LegacyRollback { num_turns: u32 }, } #[derive(Debug)] @@ -474,7 +472,6 @@ struct ThreadEventStore { turns: Vec, buffer: VecDeque, pending_interactive_replay: PendingInteractiveReplayState, - pending_local_legacy_rollbacks: VecDeque, active_turn_id: Option, input_state: Option, capacity: usize, @@ -483,10 +480,7 @@ struct ThreadEventStore { impl ThreadEventStore { fn event_survives_session_refresh(event: &ThreadBufferedEvent) -> bool { - matches!( - event, - ThreadBufferedEvent::Request(_) | ThreadBufferedEvent::LegacyWarning(_) - ) + matches!(event, ThreadBufferedEvent::Request(_)) } fn new(capacity: usize) -> Self { @@ -495,7 +489,6 @@ impl ThreadEventStore { turns: Vec::new(), buffer: VecDeque::new(), pending_interactive_replay: PendingInteractiveReplayState::default(), - pending_local_legacy_rollbacks: VecDeque::new(), active_turn_id: None, input_state: None, capacity, @@ -521,7 +514,6 @@ impl ThreadEventStore { } fn set_turns(&mut self, turns: Vec) { - self.pending_local_legacy_rollbacks.clear(); self.active_turn_id = turns .iter() .rev() @@ -578,37 +570,6 @@ impl ThreadEventStore { self.active_turn_id = None; } - fn note_local_thread_rollback(&mut self, num_turns: u32) { - self.pending_local_legacy_rollbacks.push_back(num_turns); - while self.pending_local_legacy_rollbacks.len() > self.capacity { - self.pending_local_legacy_rollbacks.pop_front(); - } - } - - fn consume_pending_local_legacy_rollback(&mut self, num_turns: u32) -> bool { - match self.pending_local_legacy_rollbacks.front() { - Some(pending_num_turns) if *pending_num_turns == num_turns => { - self.pending_local_legacy_rollbacks.pop_front(); - true - } - _ => false, - } - } - - fn apply_legacy_thread_rollback(&mut self, num_turns: u32) { - let num_turns = usize::try_from(num_turns).unwrap_or(usize::MAX); - if num_turns >= self.turns.len() { - self.turns.clear(); - } else { - self.turns - .truncate(self.turns.len().saturating_sub(num_turns)); - } - self.buffer.clear(); - self.pending_interactive_replay = PendingInteractiveReplayState::default(); - self.pending_local_legacy_rollbacks.clear(); - self.active_turn_id = None; - } - fn snapshot(&self) -> ThreadEventSnapshot { ThreadEventSnapshot { session: self.session.clone(), @@ -623,9 +584,7 @@ impl ThreadEventStore { .pending_interactive_replay .should_replay_snapshot_request(request), ThreadBufferedEvent::Notification(_) - | ThreadBufferedEvent::HistoryEntryResponse(_) - | ThreadBufferedEvent::LegacyWarning(_) - | ThreadBufferedEvent::LegacyRollback { .. } => true, + | ThreadBufferedEvent::HistoryEntryResponse(_) => true, }) .cloned() .collect(), @@ -2283,50 +2242,6 @@ impl App { Ok(()) } - async fn enqueue_thread_legacy_warning( - &mut self, - thread_id: ThreadId, - message: String, - ) -> Result<()> { - let (sender, store) = { - let channel = self.ensure_thread_channel(thread_id); - (channel.sender.clone(), Arc::clone(&channel.store)) - }; - - let should_send = { - let mut guard = store.lock().await; - guard - .buffer - .push_back(ThreadBufferedEvent::LegacyWarning(message.clone())); - if guard.buffer.len() > guard.capacity - && let Some(removed) = guard.buffer.pop_front() - && let ThreadBufferedEvent::Request(request) = &removed - { - guard - .pending_interactive_replay - .note_evicted_server_request(request); - } - guard.active - }; - - if should_send { - match sender.try_send(ThreadBufferedEvent::LegacyWarning(message)) { - Ok(()) => {} - Err(TrySendError::Full(event)) => { - tokio::spawn(async move { - if let Err(err) = sender.send(event).await { - tracing::warn!("thread {thread_id} event channel closed: {err}"); - } - }); - } - Err(TrySendError::Closed(_)) => { - tracing::warn!("thread {thread_id} event channel closed"); - } - } - } - Ok(()) - } - async fn enqueue_thread_history_entry_response( &mut self, thread_id: ThreadId, @@ -2371,64 +2286,6 @@ impl App { Ok(()) } - async fn enqueue_thread_legacy_rollback( - &mut self, - thread_id: ThreadId, - num_turns: u32, - ) -> Result<()> { - let (sender, store) = { - let channel = self.ensure_thread_channel(thread_id); - (channel.sender.clone(), Arc::clone(&channel.store)) - }; - - let should_send = { - let mut guard = store.lock().await; - if guard.consume_pending_local_legacy_rollback(num_turns) { - false - } else { - guard.apply_legacy_thread_rollback(num_turns); - guard.active - } - }; - - if should_send { - match sender.try_send(ThreadBufferedEvent::LegacyRollback { num_turns }) { - Ok(()) => {} - Err(TrySendError::Full(event)) => { - tokio::spawn(async move { - if let Err(err) = sender.send(event).await { - tracing::warn!("thread {thread_id} event channel closed: {err}"); - } - }); - } - Err(TrySendError::Closed(_)) => { - tracing::warn!("thread {thread_id} event channel closed"); - } - } - } - Ok(()) - } - - async fn enqueue_primary_thread_legacy_warning(&mut self, message: String) -> Result<()> { - if let Some(thread_id) = self.primary_thread_id { - return self.enqueue_thread_legacy_warning(thread_id, message).await; - } - self.pending_primary_events - .push_back(ThreadBufferedEvent::LegacyWarning(message)); - Ok(()) - } - - async fn enqueue_primary_thread_legacy_rollback(&mut self, num_turns: u32) -> Result<()> { - if let Some(thread_id) = self.primary_thread_id { - return self - .enqueue_thread_legacy_rollback(thread_id, num_turns) - .await; - } - self.pending_primary_events - .push_back(ThreadBufferedEvent::LegacyRollback { num_turns }); - Ok(()) - } - async fn enqueue_primary_thread_session( &mut self, session: ThreadSessionState, @@ -2466,14 +2323,6 @@ impl App { self.enqueue_thread_history_entry_response(thread_id, event) .await?; } - ThreadBufferedEvent::LegacyWarning(message) => { - self.enqueue_thread_legacy_warning(thread_id, message) - .await?; - } - ThreadBufferedEvent::LegacyRollback { num_turns } => { - self.enqueue_thread_legacy_rollback(thread_id, num_turns) - .await?; - } } } self.chat_widget @@ -4769,7 +4618,6 @@ impl App { if let Some(channel) = self.thread_event_channels.get(&thread_id) { let mut store = channel.store.lock().await; store.apply_thread_rollback(response); - store.note_local_thread_rollback(num_turns); } if self.active_thread_id == Some(thread_id) && let Some(mut rx) = self.active_thread_rx.take() @@ -4814,13 +4662,6 @@ impl App { ThreadBufferedEvent::HistoryEntryResponse(event) => { self.chat_widget.handle_history_entry_response(event); } - ThreadBufferedEvent::LegacyWarning(message) => { - self.chat_widget.add_warning_message(message); - } - ThreadBufferedEvent::LegacyRollback { num_turns } => { - self.handle_backtrack_rollback_succeeded(num_turns); - self.chat_widget.handle_thread_rolled_back(); - } } if needs_refresh { self.refresh_status_line(); @@ -4838,13 +4679,6 @@ impl App { ThreadBufferedEvent::HistoryEntryResponse(event) => { self.chat_widget.handle_history_entry_response(event) } - ThreadBufferedEvent::LegacyWarning(message) => { - self.chat_widget.add_warning_message(message); - } - ThreadBufferedEvent::LegacyRollback { num_turns } => { - self.handle_backtrack_rollback_succeeded(num_turns); - self.chat_widget.handle_thread_rolled_back(); - } } } @@ -5335,6 +5169,7 @@ mod tests { use codex_app_server_protocol::AdditionalPermissionProfile; use codex_app_server_protocol::AgentMessageDeltaNotification; use codex_app_server_protocol::CommandExecutionRequestApprovalParams; + use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::NetworkApprovalContext as AppServerNetworkApprovalContext; use codex_app_server_protocol::NetworkApprovalProtocol as AppServerNetworkApprovalProtocol; use codex_app_server_protocol::NetworkPolicyAmendment as AppServerNetworkPolicyAmendment; @@ -5935,33 +5770,6 @@ mod tests { } } - #[tokio::test] - async fn replay_thread_snapshot_replays_legacy_warning_history() { - let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; - - app.replay_thread_snapshot( - ThreadEventSnapshot { - session: None, - turns: Vec::new(), - events: vec![ThreadBufferedEvent::LegacyWarning( - "legacy warning message".to_string(), - )], - input_state: None, - }, - false, - ); - - let mut saw_warning = false; - while let Ok(event) = app_event_rx.try_recv() { - if let AppEvent::InsertHistoryCell(cell) = event { - let transcript = lines_to_single_string(&cell.transcript_lines(80)); - saw_warning |= transcript.contains("legacy warning message"); - } - } - - assert!(saw_warning, "expected replayed legacy warning history cell"); - } - #[tokio::test] async fn replay_only_thread_keeps_restored_queue_visible() { let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await; @@ -7308,87 +7116,6 @@ guardian_approval = true Ok(()) } - #[tokio::test] - async fn legacy_warning_eviction_clears_pending_interactive_replay_state() -> Result<()> { - let mut app = make_test_app().await; - let thread_id = ThreadId::new(); - let channel = ThreadEventChannel::new(1); - { - let mut store = channel.store.lock().await; - store.push_request(exec_approval_request( - thread_id, - "turn-approval", - "call-approval", - None, - )); - assert_eq!(store.has_pending_thread_approvals(), true); - } - app.thread_event_channels.insert(thread_id, channel); - - app.enqueue_thread_legacy_warning(thread_id, "legacy warning".to_string()) - .await?; - - let store = app - .thread_event_channels - .get(&thread_id) - .expect("thread store should exist") - .store - .lock() - .await; - assert_eq!(store.has_pending_thread_approvals(), false); - let snapshot = store.snapshot(); - assert_eq!(snapshot.events.len(), 1); - assert!(matches!( - snapshot.events.first(), - Some(ThreadBufferedEvent::LegacyWarning(message)) if message == "legacy warning" - )); - - Ok(()) - } - - #[tokio::test] - async fn legacy_thread_rollback_trims_inactive_thread_snapshot_state() -> Result<()> { - let mut app = make_test_app().await; - let thread_id = ThreadId::new(); - let session = test_thread_session(thread_id, PathBuf::from("/tmp/project")); - let turns = vec![ - test_turn("turn-1", TurnStatus::Completed, Vec::new()), - test_turn("turn-2", TurnStatus::Completed, Vec::new()), - ]; - let channel = ThreadEventChannel::new_with_session(4, session, turns); - { - let mut store = channel.store.lock().await; - store.push_request(exec_approval_request( - thread_id, - "turn-approval", - "call-approval", - None, - )); - assert_eq!(store.has_pending_thread_approvals(), true); - } - app.thread_event_channels.insert(thread_id, channel); - - app.enqueue_thread_legacy_rollback(thread_id, 1).await?; - - let store = app - .thread_event_channels - .get(&thread_id) - .expect("thread store should exist") - .store - .lock() - .await; - assert_eq!( - store.turns, - vec![test_turn("turn-1", TurnStatus::Completed, Vec::new())] - ); - assert_eq!(store.has_pending_thread_approvals(), false); - let snapshot = store.snapshot(); - assert_eq!(snapshot.turns, store.turns); - assert!(snapshot.events.is_empty()); - - Ok(()) - } - #[tokio::test] async fn inactive_thread_started_notification_initializes_replay_session() -> Result<()> { let mut app = make_test_app().await; @@ -8108,16 +7835,6 @@ guardian_approval = true assert_eq!(store.has_pending_thread_approvals(), false); } - #[test] - fn thread_event_store_consumes_matching_local_legacy_rollback_once() { - let mut store = ThreadEventStore::new(8); - store.note_local_thread_rollback(2); - - assert!(store.consume_pending_local_legacy_rollback(2)); - assert!(!store.consume_pending_local_legacy_rollback(2)); - assert!(!store.consume_pending_local_legacy_rollback(1)); - } - fn next_user_turn_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Op { let mut seen = Vec::new(); while let Ok(op) = op_rx.try_recv() { @@ -9076,8 +8793,13 @@ guardian_approval = true let (tx, rx) = mpsc::channel(8); app.active_thread_id = Some(thread_id); app.active_thread_rx = Some(rx); - tx.send(ThreadBufferedEvent::LegacyWarning( - "stale warning".to_string(), + tx.send(ThreadBufferedEvent::Notification( + ServerNotification::ConfigWarning(ConfigWarningNotification { + summary: "stale warning".to_string(), + details: None, + path: None, + range: None, + }), )) .await .expect("event should queue"); @@ -9115,62 +8837,6 @@ guardian_approval = true assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty))); } - #[tokio::test] - async fn local_rollback_response_suppresses_matching_legacy_rollback() { - let mut app = make_test_app().await; - let thread_id = ThreadId::new(); - let session = test_thread_session(thread_id, PathBuf::from("/tmp/project")); - let initial_turns = vec![ - test_turn("turn-1", TurnStatus::Completed, Vec::new()), - test_turn("turn-2", TurnStatus::Completed, Vec::new()), - ]; - app.thread_event_channels.insert( - thread_id, - ThreadEventChannel::new_with_session(8, session, initial_turns), - ); - - app.handle_thread_rollback_response( - thread_id, - 1, - &ThreadRollbackResponse { - thread: Thread { - id: thread_id.to_string(), - preview: String::new(), - ephemeral: false, - model_provider: "openai".to_string(), - created_at: 0, - updated_at: 0, - status: codex_app_server_protocol::ThreadStatus::Idle, - path: None, - cwd: PathBuf::from("/tmp/project"), - cli_version: "0.0.0".to_string(), - source: SessionSource::Cli.into(), - agent_nickname: None, - agent_role: None, - git_info: None, - name: None, - turns: vec![test_turn("turn-1", TurnStatus::Completed, Vec::new())], - }, - }, - ) - .await; - - app.enqueue_thread_legacy_rollback(thread_id, 1) - .await - .expect("legacy rollback should not fail"); - - let store = app - .thread_event_channels - .get(&thread_id) - .expect("thread channel") - .store - .lock() - .await; - let snapshot = store.snapshot(); - assert_eq!(snapshot.turns.len(), 1); - assert!(snapshot.events.is_empty()); - } - #[tokio::test] async fn new_session_requests_shutdown_for_previous_conversation() { let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; diff --git a/codex-rs/tui_app_server/src/app/app_server_adapter.rs b/codex-rs/tui_app_server/src/app/app_server_adapter.rs index 0d21128538..952a9bf7b8 100644 --- a/codex-rs/tui_app_server/src/app/app_server_adapter.rs +++ b/codex-rs/tui_app_server/src/app/app_server_adapter.rs @@ -16,13 +16,9 @@ use crate::app_event::AppEvent; use crate::app_server_session::AppServerSession; use crate::app_server_session::app_server_rate_limit_snapshot_to_core; use crate::app_server_session::status_account_display_from_auth_mode; -use crate::local_chatgpt_auth::load_local_chatgpt_auth; use codex_app_server_client::AppServerEvent; use codex_app_server_protocol::AuthMode; -use codex_app_server_protocol::ChatgptAuthTokensRefreshParams; use codex_app_server_protocol::JSONRPCErrorError; -use codex_app_server_protocol::JSONRPCNotification; -use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequest; #[cfg(test)] @@ -106,16 +102,9 @@ use codex_protocol::protocol::TurnAbortedEvent; use codex_protocol::protocol::TurnCompleteEvent; #[cfg(test)] use codex_protocol::protocol::TurnStartedEvent; -use serde_json::Value; #[cfg(test)] use std::time::Duration; -#[derive(Debug, PartialEq, Eq)] -enum LegacyThreadNotification { - Warning(String), - Rollback { num_turns: u32 }, -} - impl App { pub(super) async fn handle_app_server_event( &mut self, @@ -133,48 +122,10 @@ impl App { self.handle_server_notification_event(app_server_client, notification) .await; } - AppServerEvent::LegacyNotification(notification) => { - if let Some((thread_id, legacy_notification)) = - legacy_thread_notification(notification) - { - let result = match legacy_notification { - LegacyThreadNotification::Warning(message) => { - if self.primary_thread_id == Some(thread_id) - || self.primary_thread_id.is_none() - { - self.enqueue_primary_thread_legacy_warning(message).await - } else { - self.enqueue_thread_legacy_warning(thread_id, message).await - } - } - LegacyThreadNotification::Rollback { num_turns } => { - if self.primary_thread_id == Some(thread_id) - || self.primary_thread_id.is_none() - { - self.enqueue_primary_thread_legacy_rollback(num_turns).await - } else { - self.enqueue_thread_legacy_rollback(thread_id, num_turns) - .await - } - } - }; - if let Err(err) = result { - tracing::warn!("failed to enqueue app-server legacy notification: {err}"); - } - } else { - tracing::debug!("ignoring legacy app-server notification in tui_app_server"); - } + AppServerEvent::LegacyNotification(_) => { + tracing::debug!("ignoring legacy app-server notification in tui_app_server"); } AppServerEvent::ServerRequest(request) => { - if let ServerRequest::ChatgptAuthTokensRefresh { request_id, params } = request { - self.handle_chatgpt_auth_tokens_refresh_request( - app_server_client, - request_id, - params, - ) - .await; - return; - } self.handle_server_request_event(app_server_client, request) .await; } @@ -293,71 +244,6 @@ impl App { tracing::warn!("failed to enqueue app-server request: {err}"); } } - - async fn handle_chatgpt_auth_tokens_refresh_request( - &mut self, - app_server_client: &AppServerSession, - request_id: RequestId, - params: ChatgptAuthTokensRefreshParams, - ) { - let config = self.config.clone(); - let result = tokio::task::spawn_blocking(move || { - resolve_chatgpt_auth_tokens_refresh_response( - &config.codex_home, - config.cli_auth_credentials_store_mode, - config.forced_chatgpt_workspace_id.as_deref(), - ¶ms, - ) - }) - .await; - - match result { - Ok(Ok(response)) => { - let response = serde_json::to_value(response).map_err(|err| { - format!("failed to serialize chatgpt auth refresh response: {err}") - }); - match response { - Ok(response) => { - if let Err(err) = app_server_client - .resolve_server_request(request_id, response) - .await - { - tracing::warn!("failed to resolve chatgpt auth refresh request: {err}"); - } - } - Err(err) => { - self.chat_widget.add_error_message(err.clone()); - if let Err(reject_err) = self - .reject_app_server_request(app_server_client, request_id, err) - .await - { - tracing::warn!("{reject_err}"); - } - } - } - } - Ok(Err(err)) => { - self.chat_widget.add_error_message(err.clone()); - if let Err(reject_err) = self - .reject_app_server_request(app_server_client, request_id, err) - .await - { - tracing::warn!("{reject_err}"); - } - } - Err(err) => { - let message = format!("chatgpt auth refresh task failed: {err}"); - self.chat_widget.add_error_message(message.clone()); - if let Err(reject_err) = self - .reject_app_server_request(app_server_client, request_id, message) - .await - { - tracing::warn!("{reject_err}"); - } - } - } - } - async fn reject_app_server_request( &self, app_server_client: &AppServerSession, @@ -482,6 +368,9 @@ fn server_notification_thread_target( ServerNotification::ThreadRealtimeItemAdded(notification) => { Some(notification.thread_id.as_str()) } + ServerNotification::ThreadRealtimeTranscriptUpdated(notification) => { + Some(notification.thread_id.as_str()) + } ServerNotification::ThreadRealtimeOutputAudioDelta(notification) => { Some(notification.thread_id.as_str()) } @@ -516,28 +405,6 @@ fn server_notification_thread_target( } } -fn resolve_chatgpt_auth_tokens_refresh_response( - codex_home: &std::path::Path, - auth_credentials_store_mode: codex_core::auth::AuthCredentialsStoreMode, - forced_chatgpt_workspace_id: Option<&str>, - params: &ChatgptAuthTokensRefreshParams, -) -> Result { - let auth = load_local_chatgpt_auth( - codex_home, - auth_credentials_store_mode, - forced_chatgpt_workspace_id, - )?; - if let Some(previous_account_id) = params.previous_account_id.as_deref() - && previous_account_id != auth.chatgpt_account_id - { - return Err(format!( - "local ChatGPT auth refresh account mismatch: expected `{previous_account_id}`, got `{}`", - auth.chatgpt_account_id - )); - } - Ok(auth.to_refresh_response()) -} - #[cfg(test)] /// Convert a `Thread` snapshot into a flat sequence of protocol `Event`s /// suitable for replaying into the TUI event store. @@ -564,48 +431,6 @@ pub(super) fn thread_snapshot_events( .collect() } -fn legacy_thread_notification( - notification: JSONRPCNotification, -) -> Option<(ThreadId, LegacyThreadNotification)> { - let method = notification - .method - .strip_prefix("codex/event/") - .unwrap_or(¬ification.method); - - let Value::Object(mut params) = notification.params? else { - return None; - }; - let thread_id = params - .remove("conversationId") - .and_then(|value| serde_json::from_value::(value).ok()) - .and_then(|value| ThreadId::from_string(&value).ok())?; - let msg = params.get("msg").and_then(Value::as_object)?; - - match method { - "warning" => { - let message = msg - .get("type") - .and_then(Value::as_str) - .zip(msg.get("message")) - .and_then(|(kind, message)| (kind == "warning").then_some(message)) - .and_then(Value::as_str) - .map(ToOwned::to_owned)?; - Some((thread_id, LegacyThreadNotification::Warning(message))) - } - "thread_rolled_back" => { - let num_turns = msg - .get("type") - .and_then(Value::as_str) - .zip(msg.get("num_turns")) - .and_then(|(kind, num_turns)| (kind == "thread_rolled_back").then_some(num_turns)) - .and_then(Value::as_u64) - .and_then(|num_turns| u32::try_from(num_turns).ok())?; - Some((thread_id, LegacyThreadNotification::Rollback { num_turns })) - } - _ => None, - } -} - #[cfg(test)] fn server_notification_thread_events( notification: ServerNotification, @@ -1150,113 +975,6 @@ fn split_command_string(command: &str) -> Vec { } } -#[cfg(test)] -mod refresh_tests { - use super::*; - - use base64::Engine; - use chrono::Utc; - use codex_app_server_protocol::AuthMode; - use codex_core::auth::AuthCredentialsStoreMode; - use codex_core::auth::AuthDotJson; - use codex_core::auth::save_auth; - use codex_core::token_data::TokenData; - use pretty_assertions::assert_eq; - use serde::Serialize; - use serde_json::json; - use tempfile::TempDir; - - fn fake_jwt(account_id: &str, plan_type: &str) -> String { - #[derive(Serialize)] - struct Header { - alg: &'static str, - typ: &'static str, - } - - let header = Header { - alg: "none", - typ: "JWT", - }; - let payload = json!({ - "email": "user@example.com", - "https://api.openai.com/auth": { - "chatgpt_account_id": account_id, - "chatgpt_plan_type": plan_type, - }, - }); - let encode = |bytes: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes); - let header_b64 = encode(&serde_json::to_vec(&header).expect("serialize header")); - let payload_b64 = encode(&serde_json::to_vec(&payload).expect("serialize payload")); - let signature_b64 = encode(b"sig"); - format!("{header_b64}.{payload_b64}.{signature_b64}") - } - - fn write_chatgpt_auth(codex_home: &std::path::Path) { - let id_token = fake_jwt("workspace-1", "business"); - let access_token = fake_jwt("workspace-1", "business"); - save_auth( - codex_home, - &AuthDotJson { - auth_mode: Some(AuthMode::Chatgpt), - openai_api_key: None, - tokens: Some(TokenData { - id_token: codex_core::token_data::parse_chatgpt_jwt_claims(&id_token) - .expect("id token should parse"), - access_token, - refresh_token: "refresh-token".to_string(), - account_id: Some("workspace-1".to_string()), - }), - last_refresh: Some(Utc::now()), - }, - AuthCredentialsStoreMode::File, - ) - .expect("chatgpt auth should save"); - } - - #[test] - fn refresh_request_uses_local_chatgpt_auth() { - let codex_home = TempDir::new().expect("tempdir"); - write_chatgpt_auth(codex_home.path()); - - let response = resolve_chatgpt_auth_tokens_refresh_response( - codex_home.path(), - AuthCredentialsStoreMode::File, - Some("workspace-1"), - &ChatgptAuthTokensRefreshParams { - reason: codex_app_server_protocol::ChatgptAuthTokensRefreshReason::Unauthorized, - previous_account_id: Some("workspace-1".to_string()), - }, - ) - .expect("refresh response should resolve"); - - assert_eq!(response.chatgpt_account_id, "workspace-1"); - assert_eq!(response.chatgpt_plan_type.as_deref(), Some("business")); - assert!(!response.access_token.is_empty()); - } - - #[test] - fn refresh_request_rejects_account_mismatch() { - let codex_home = TempDir::new().expect("tempdir"); - write_chatgpt_auth(codex_home.path()); - - let err = resolve_chatgpt_auth_tokens_refresh_response( - codex_home.path(), - AuthCredentialsStoreMode::File, - Some("workspace-1"), - &ChatgptAuthTokensRefreshParams { - reason: codex_app_server_protocol::ChatgptAuthTokensRefreshReason::Unauthorized, - previous_account_id: Some("workspace-2".to_string()), - }, - ) - .expect_err("mismatched account should fail"); - - assert_eq!( - err, - "local ChatGPT auth refresh account mismatch: expected `workspace-2`, got `workspace-1`" - ); - } -} - #[cfg(test)] fn app_server_web_search_action_to_core( action: codex_app_server_protocol::WebSearchAction, @@ -1286,9 +1004,7 @@ fn app_server_codex_error_info_to_core( #[cfg(test)] mod tests { - use super::LegacyThreadNotification; use super::command_execution_started_event; - use super::legacy_thread_notification; use super::server_notification_thread_events; use super::thread_snapshot_events; use super::turn_snapshot_events; @@ -1300,7 +1016,6 @@ mod tests { use codex_app_server_protocol::CommandExecutionStatus; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; - use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::Thread; @@ -1321,57 +1036,8 @@ mod tests { use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnAbortedEvent; use pretty_assertions::assert_eq; - use serde_json::json; use std::path::PathBuf; - #[test] - fn legacy_warning_notification_extracts_thread_id_and_message() { - let thread_id = ThreadId::new(); - let warning = legacy_thread_notification(JSONRPCNotification { - method: "codex/event/warning".to_string(), - params: Some(json!({ - "conversationId": thread_id.to_string(), - "id": "event-1", - "msg": { - "type": "warning", - "message": "legacy warning message", - }, - })), - }); - - assert_eq!( - warning, - Some(( - thread_id, - LegacyThreadNotification::Warning("legacy warning message".to_string()) - )) - ); - } - - #[test] - fn legacy_thread_rollback_notification_extracts_thread_id_and_turn_count() { - let thread_id = ThreadId::new(); - let rollback = legacy_thread_notification(JSONRPCNotification { - method: "codex/event/thread_rolled_back".to_string(), - params: Some(json!({ - "conversationId": thread_id.to_string(), - "id": "event-1", - "msg": { - "type": "thread_rolled_back", - "num_turns": 2, - }, - })), - }); - - assert_eq!( - rollback, - Some(( - thread_id, - LegacyThreadNotification::Rollback { num_turns: 2 } - )) - ); - } - #[test] fn bridges_completed_agent_messages_from_server_notifications() { let thread_id = "019cee8c-b993-7e33-88c0-014d4e62612d".to_string(); diff --git a/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs b/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs index b86c029b5a..0e1f3e08d6 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs @@ -403,6 +403,7 @@ pub(crate) struct ChatComposer { config: ChatComposerConfig, collaboration_mode_indicator: Option, connectors_enabled: bool, + plugins_command_enabled: bool, fast_command_enabled: bool, personality_command_enabled: bool, realtime_conversation_enabled: bool, @@ -441,6 +442,7 @@ impl ChatComposer { BuiltinCommandFlags { collaboration_modes_enabled: self.collaboration_modes_enabled, connectors_enabled: self.connectors_enabled, + plugins_command_enabled: self.plugins_command_enabled, fast_command_enabled: self.fast_command_enabled, personality_command_enabled: self.personality_command_enabled, realtime_conversation_enabled: self.realtime_conversation_enabled, @@ -525,6 +527,7 @@ impl ChatComposer { config, collaboration_mode_indicator: None, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, @@ -559,6 +562,10 @@ impl ChatComposer { self.sync_popups(); } + pub fn set_plugins_command_enabled(&mut self, enabled: bool) { + self.plugins_command_enabled = enabled; + } + /// Toggle composer-side image paste handling. /// /// This only affects whether image-like paste content is converted into attachments; the @@ -3491,6 +3498,7 @@ impl ChatComposer { if is_editing_slash_command_name { let collaboration_modes_enabled = self.collaboration_modes_enabled; let connectors_enabled = self.connectors_enabled; + let plugins_command_enabled = self.plugins_command_enabled; let fast_command_enabled = self.fast_command_enabled; let personality_command_enabled = self.personality_command_enabled; let realtime_conversation_enabled = self.realtime_conversation_enabled; @@ -3500,6 +3508,7 @@ impl ChatComposer { CommandPopupFlags { collaboration_modes_enabled, connectors_enabled, + plugins_command_enabled, fast_command_enabled, personality_command_enabled, realtime_conversation_enabled, diff --git a/codex-rs/tui_app_server/src/bottom_pane/command_popup.rs b/codex-rs/tui_app_server/src/bottom_pane/command_popup.rs index 05b15b7935..36cc87668c 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/command_popup.rs @@ -38,6 +38,7 @@ pub(crate) struct CommandPopup { pub(crate) struct CommandPopupFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) connectors_enabled: bool, + pub(crate) plugins_command_enabled: bool, pub(crate) fast_command_enabled: bool, pub(crate) personality_command_enabled: bool, pub(crate) realtime_conversation_enabled: bool, @@ -50,6 +51,7 @@ impl From for slash_commands::BuiltinCommandFlags { Self { collaboration_modes_enabled: value.collaboration_modes_enabled, connectors_enabled: value.connectors_enabled, + plugins_command_enabled: value.plugins_command_enabled, fast_command_enabled: value.fast_command_enabled, personality_command_enabled: value.personality_command_enabled, realtime_conversation_enabled: value.realtime_conversation_enabled, @@ -510,6 +512,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -532,6 +535,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -554,6 +558,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: false, realtime_conversation_enabled: false, @@ -584,6 +589,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: false, @@ -606,6 +612,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: false, connectors_enabled: false, + plugins_command_enabled: false, fast_command_enabled: false, personality_command_enabled: true, realtime_conversation_enabled: true, diff --git a/codex-rs/tui_app_server/src/bottom_pane/mod.rs b/codex-rs/tui_app_server/src/bottom_pane/mod.rs index c7d63be402..2531f8586b 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/mod.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/mod.rs @@ -262,6 +262,11 @@ impl BottomPane { self.request_redraw(); } + pub fn set_plugins_command_enabled(&mut self, enabled: bool) { + self.composer.set_plugins_command_enabled(enabled); + self.request_redraw(); + } + pub fn take_mention_bindings(&mut self) -> Vec { self.composer.take_mention_bindings() } diff --git a/codex-rs/tui_app_server/src/bottom_pane/pending_input_preview.rs b/codex-rs/tui_app_server/src/bottom_pane/pending_input_preview.rs index 315e311e02..1f38a17773 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/pending_input_preview.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/pending_input_preview.rs @@ -176,6 +176,21 @@ mod tests { assert_snapshot!("render_one_message", format!("{buf:?}")); } + #[test] + fn render_one_message_with_shift_left_binding() { + let mut queue = PendingInputPreview::new(); + queue.queued_messages.push("Hello, world!".to_string()); + queue.set_edit_binding(key_hint::shift(KeyCode::Left)); + let width = 40; + let height = queue.desired_height(width); + let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); + queue.render(Rect::new(0, 0, width, height), &mut buf); + assert_snapshot!( + "render_one_message_with_shift_left_binding", + format!("{buf:?}") + ); + } + #[test] fn render_two_messages() { let mut queue = PendingInputPreview::new(); diff --git a/codex-rs/tui_app_server/src/bottom_pane/slash_commands.rs b/codex-rs/tui_app_server/src/bottom_pane/slash_commands.rs index 15b70f232c..54b1a8cf4e 100644 --- a/codex-rs/tui_app_server/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui_app_server/src/bottom_pane/slash_commands.rs @@ -14,6 +14,7 @@ use crate::slash_command::built_in_slash_commands; pub(crate) struct BuiltinCommandFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) connectors_enabled: bool, + pub(crate) plugins_command_enabled: bool, pub(crate) fast_command_enabled: bool, pub(crate) personality_command_enabled: bool, pub(crate) realtime_conversation_enabled: bool, @@ -31,6 +32,7 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st || !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan) }) .filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps) + .filter(|(_, cmd)| flags.plugins_command_enabled || *cmd != SlashCommand::Plugins) .filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast) .filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality) .filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime) @@ -63,6 +65,7 @@ mod tests { BuiltinCommandFlags { collaboration_modes_enabled: true, connectors_enabled: true, + plugins_command_enabled: true, fast_command_enabled: true, personality_command_enabled: true, realtime_conversation_enabled: true, diff --git a/codex-rs/tui_app_server/src/bottom_pane/snapshots/codex_tui_app_server__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap b/codex-rs/tui_app_server/src/bottom_pane/snapshots/codex_tui_app_server__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap new file mode 100644 index 0000000000..d70db56644 --- /dev/null +++ b/codex-rs/tui_app_server/src/bottom_pane/snapshots/codex_tui_app_server__bottom_pane__pending_input_preview__tests__render_one_message_with_shift_left_binding.snap @@ -0,0 +1,21 @@ +--- +source: tui_app_server/src/bottom_pane/pending_input_preview.rs +expression: "format!(\"{buf:?}\")" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 40, height: 3 }, + content: [ + "• Queued follow-up messages ", + " ↳ Hello, world! ", + " shift + ← edit last queued message ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC, + x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} diff --git a/codex-rs/tui_app_server/src/chatwidget.rs b/codex-rs/tui_app_server/src/chatwidget.rs index 5e0cff03c7..78fcc84e5e 100644 --- a/codex-rs/tui_app_server/src/chatwidget.rs +++ b/codex-rs/tui_app_server/src/chatwidget.rs @@ -199,6 +199,8 @@ use codex_protocol::request_user_input::RequestUserInputEvent; use codex_protocol::request_user_input::RequestUserInputQuestionOption; use codex_protocol::user_input::TextElement; use codex_protocol::user_input::UserInput; +use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_terminal_detection::terminal_info; use codex_utils_sleep_inhibitor::SleepInhibitor; @@ -238,14 +240,21 @@ const APP_SERVER_TUI_STUB_MESSAGE: &str = "Not available in app-server TUI yet." /// Choose the keybinding used to edit the most-recently queued message. /// /// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently -/// swallow Alt+Up, so users in those environments would never be able to trigger -/// the edit action. We fall back to Shift+Left for those terminals while -/// keeping the more discoverable Alt+Up everywhere else. +/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall +/// back to Shift+Left for those environments while keeping the more discoverable +/// Alt+Up everywhere else. /// /// The match is exhaustive so that adding a new `TerminalName` variant forces /// an explicit decision about which binding that terminal should use. -fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding { - match terminal_name { +fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding { + if matches!( + terminal_info.multiplexer.as_ref(), + Some(Multiplexer::Tmux { .. }) + ) { + return key_hint::shift(KeyCode::Left); + } + + match terminal_info.name { TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => { key_hint::shift(KeyCode::Left) } @@ -1791,6 +1800,7 @@ impl ChatWidget { self.refresh_model_display(); self.sync_fast_command_enabled(); self.sync_personality_command_enabled(); + self.sync_plugins_command_enabled(); self.refresh_plugin_mentions(); let startup_tooltip_override = self.startup_tooltip_override.take(); let show_fast_status = self.should_show_fast_status(&model_for_header, event.service_tier); @@ -4169,8 +4179,7 @@ impl ChatWidget { let active_cell = Some(Self::placeholder_session_header_cell(&config)); let current_cwd = Some(config.cwd.clone()); - let queued_message_edit_binding = - queued_message_edit_binding_for_terminal(terminal_info().name); + let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info()); let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), @@ -4290,6 +4299,7 @@ impl ChatWidget { .set_collaboration_modes_enabled(/*enabled*/ true); widget.sync_fast_command_enabled(); widget.sync_personality_command_enabled(); + widget.sync_plugins_command_enabled(); widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_binding); @@ -6069,6 +6079,7 @@ impl ChatWidget { | ServerNotification::ContextCompacted(_) | ServerNotification::FuzzyFileSearchSessionUpdated(_) | ServerNotification::FuzzyFileSearchSessionCompleted(_) + | ServerNotification::ThreadRealtimeTranscriptUpdated(_) | ServerNotification::WindowsWorldWritableWarning(_) | ServerNotification::WindowsSandboxSetupCompleted(_) | ServerNotification::AccountLoginCompleted(_) => {} @@ -9002,6 +9013,7 @@ impl ChatWidget { self.sync_personality_command_enabled(); } if feature == Feature::Plugins { + self.sync_plugins_command_enabled(); self.refresh_plugin_mentions(); } if feature == Feature::PreventIdleSleep { @@ -9232,6 +9244,11 @@ impl ChatWidget { .set_personality_command_enabled(self.config.features.enabled(Feature::Personality)); } + fn sync_plugins_command_enabled(&mut self) { + self.bottom_pane + .set_plugins_command_enabled(self.config.features.enabled(Feature::Plugins)); + } + fn current_model_supports_personality(&self) -> bool { let model = self.current_model(); self.model_catalog @@ -9539,10 +9556,6 @@ impl ChatWidget { self.request_redraw(); } - pub(crate) fn add_warning_message(&mut self, message: String) { - self.on_warning(message); - } - fn add_app_server_stub_message(&mut self, feature: &str) { warn!(feature, "stubbed unsupported app-server TUI feature"); self.add_error_message(format!("{feature}: {APP_SERVER_TUI_STUB_MESSAGE}")); diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index 639b57da09..31e94249a7 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -143,6 +143,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; use codex_protocol::user_input::TextElement; use codex_protocol::user_input::UserInput; +use codex_terminal_detection::Multiplexer; +use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_approval_presets::builtin_approval_presets; @@ -3765,10 +3767,10 @@ async fn alt_up_edits_most_recent_queued_message() { } async fn assert_shift_left_edits_most_recent_queued_message_for_terminal( - terminal_name: TerminalName, + terminal_info: TerminalInfo, ) { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name); + chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info); chat.bottom_pane .set_queued_message_edit_binding(chat.queued_message_edit_binding); @@ -3800,37 +3802,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal( #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal) - .await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::AppleTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; } #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal) - .await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::WarpTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; } #[tokio::test] async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() { - assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await; + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::VsCode, + term_program: None, + version: None, + term: None, + multiplexer: None, + }) + .await; +} + +#[tokio::test] +async fn shift_left_edits_most_recent_queued_message_in_tmux() { + assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: Some(Multiplexer::Tmux { version: None }), + }) + .await; } #[test] -fn queued_message_edit_binding_mapping_covers_special_terminals() { +fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() { assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::AppleTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::WarpTerminal, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::VsCode), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::VsCode, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::shift(KeyCode::Left) ); assert_eq!( - queued_message_edit_binding_for_terminal(TerminalName::Iterm2), + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: Some(Multiplexer::Tmux { version: None }), + }), + crate::key_hint::shift(KeyCode::Left) + ); + assert_eq!( + queued_message_edit_binding_for_terminal(TerminalInfo { + name: TerminalName::Iterm2, + term_program: None, + version: None, + term: None, + multiplexer: None, + }), crate::key_hint::alt(KeyCode::Up) ); } diff --git a/codex-rs/tui_app_server/src/local_chatgpt_auth.rs b/codex-rs/tui_app_server/src/local_chatgpt_auth.rs index 6fbed6cc79..33d6e508a9 100644 --- a/codex-rs/tui_app_server/src/local_chatgpt_auth.rs +++ b/codex-rs/tui_app_server/src/local_chatgpt_auth.rs @@ -1,7 +1,6 @@ use std::path::Path; use codex_app_server_protocol::AuthMode; -use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse; use codex_core::auth::AuthCredentialsStoreMode; use codex_core::auth::load_auth_dot_json; @@ -12,16 +11,6 @@ pub(crate) struct LocalChatgptAuth { pub(crate) chatgpt_plan_type: Option, } -impl LocalChatgptAuth { - pub(crate) fn to_refresh_response(&self) -> ChatgptAuthTokensRefreshResponse { - ChatgptAuthTokensRefreshResponse { - access_token: self.access_token.clone(), - chatgpt_account_id: self.chatgpt_account_id.clone(), - chatgpt_plan_type: self.chatgpt_plan_type.clone(), - } - } -} - pub(crate) fn load_local_chatgpt_auth( codex_home: &Path, auth_credentials_store_mode: AuthCredentialsStoreMode, diff --git a/codex-rs/v8-poc/.gitignore b/codex-rs/v8-poc/.gitignore new file mode 100644 index 0000000000..8384df19c7 --- /dev/null +++ b/codex-rs/v8-poc/.gitignore @@ -0,0 +1,2 @@ +/target/ +/target-rs/ diff --git a/codex-rs/v8-poc/BUILD.bazel b/codex-rs/v8-poc/BUILD.bazel new file mode 100644 index 0000000000..0cadbd518d --- /dev/null +++ b/codex-rs/v8-poc/BUILD.bazel @@ -0,0 +1,12 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "v8-poc", + crate_name = "codex_v8_poc", + deps_extra = ["@crates//:v8"], +) + +alias( + name = "v8-poc-rusty-v8", + actual = ":v8-poc", +) diff --git a/codex-rs/v8-poc/Cargo.toml b/codex-rs/v8-poc/Cargo.toml new file mode 100644 index 0000000000..4bf008c095 --- /dev/null +++ b/codex-rs/v8-poc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "codex-v8-poc" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +name = "codex_v8_poc" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +v8 = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/v8-poc/src/lib.rs b/codex-rs/v8-poc/src/lib.rs new file mode 100644 index 0000000000..0e9faaab10 --- /dev/null +++ b/codex-rs/v8-poc/src/lib.rs @@ -0,0 +1,65 @@ +//! Bazel-wired proof-of-concept crate reserved for future V8 experiments. + +/// Returns the Bazel label for this proof-of-concept crate. +#[must_use] +pub fn bazel_target() -> &'static str { + "//codex-rs/v8-poc:v8-poc" +} + +/// Returns the embedded V8 version. +#[must_use] +pub fn embedded_v8_version() -> &'static str { + v8::V8::get_version() +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use std::sync::Once; + + use super::bazel_target; + + fn initialize_v8() { + static INIT: Once = Once::new(); + + INIT.call_once(|| { + v8::V8::initialize_platform(v8::new_default_platform(0, false).make_shared()); + v8::V8::initialize(); + }); + } + + fn evaluate_expression(expression: &str) -> String { + initialize_v8(); + + let isolate = &mut v8::Isolate::new(Default::default()); + v8::scope!(let scope, isolate); + + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let source = v8::String::new(scope, expression).expect("expression should be valid UTF-8"); + let script = v8::Script::compile(scope, source, None).expect("expression should compile"); + let result = script.run(scope).expect("expression should evaluate"); + + result.to_rust_string_lossy(scope) + } + + #[test] + fn exposes_expected_bazel_target() { + assert_eq!(bazel_target(), "//codex-rs/v8-poc:v8-poc"); + } + + #[test] + fn exposes_embedded_v8_version() { + assert!(!super::embedded_v8_version().is_empty()); + } + + #[test] + fn evaluates_integer_addition() { + assert_eq!(evaluate_expression("1 + 2"), "3"); + } + + #[test] + fn evaluates_string_concatenation() { + assert_eq!(evaluate_expression("'hello ' + 'world'"), "hello world"); + } +} diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel index 339c54a657..f308d260c0 100644 --- a/patches/BUILD.bazel +++ b/patches/BUILD.bazel @@ -1,5 +1,6 @@ exports_files([ "aws-lc-sys_memcmp_check.patch", + "rusty_v8_prebuilt_out_dir.patch", "v8_bazel_rules.patch", "v8_module_deps.patch", "v8_source_portability.patch", diff --git a/patches/aws-lc-sys_memcmp_check.patch b/patches/aws-lc-sys_memcmp_check.patch index 6d652b9edc..e9d0a441fd 100644 --- a/patches/aws-lc-sys_memcmp_check.patch +++ b/patches/aws-lc-sys_memcmp_check.patch @@ -44,7 +44,7 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs self.manifest_dir .join("aws-lc") @@ -742,6 +761,40 @@ - } + ); let _ = fs::remove_file(exec_path); } + @@ -84,3 +84,31 @@ diff --git a/builder/cc_builder.rs b/builder/cc_builder.rs fn run_compiler_checks(&self, cc_build: &mut cc::Build) { if self.compiler_check("stdalign_check", Vec::<&'static str>::new()) { cc_build.define("AWS_LC_STDALIGN_AVAILABLE", Some("1")); +diff --git a/builder/main.rs b/builder/main.rs +--- a/builder/main.rs ++++ b/builder/main.rs +@@ -944,10 +944,12 @@ + // iterate over all the include paths and copy them into the final output + for path in include_paths { + for child in std::fs::read_dir(path).into_iter().flatten().flatten() { +- if child.file_type().map_or(false, |t| t.is_file()) { ++ let child_path = child.path(); ++ ++ if child_path.is_file() { + std::fs::copy( +- child.path(), +- include_dir.join(child.path().file_name().unwrap()), ++ &child_path, ++ include_dir.join(child_path.file_name().unwrap()), + ) + .expect("Failed to copy include file during build setup"); + continue; +@@ -957,7 +959,7 @@ + let options = fs_extra::dir::CopyOptions::new() + .skip_exist(true) + .copy_inside(true); +- fs_extra::dir::copy(child.path(), &include_dir, &options) ++ fs_extra::dir::copy(child_path, &include_dir, &options) + .expect("Failed to copy include directory during build setup"); + } + } diff --git a/patches/rusty_v8_prebuilt_out_dir.patch b/patches/rusty_v8_prebuilt_out_dir.patch new file mode 100644 index 0000000000..e5132d664f --- /dev/null +++ b/patches/rusty_v8_prebuilt_out_dir.patch @@ -0,0 +1,52 @@ +--- a/build.rs ++++ b/build.rs +@@ -577,7 +577,23 @@ + path + } + ++fn out_dir_abs() -> PathBuf { ++ let cwd = env::current_dir().unwrap(); ++ ++ // target/debug/build/rusty_v8-d9e5a424d4f96994/out/ ++ let out_dir = env::var_os("OUT_DIR").expect( ++ "The 'OUT_DIR' environment is not set (it should be something like \ ++ 'target/debug/rusty_v8-{hash}').", ++ ); ++ ++ cwd.join(out_dir) ++} ++ + fn static_lib_dir() -> PathBuf { ++ if env::var_os("RUSTY_V8_ARCHIVE").is_some() { ++ return out_dir_abs().join("gn_out").join("obj"); ++ } ++ + build_dir().join("gn_out").join("obj") + } + +@@ -794,22 +810,23 @@ + } + + fn print_link_flags() { ++ let target = env::var("TARGET").unwrap(); + println!("cargo:rustc-link-lib=static=rusty_v8"); + let should_dyn_link_libcxx = env::var("CARGO_FEATURE_USE_CUSTOM_LIBCXX") + .is_err() ++ || (target.contains("apple") && env::var("RUSTY_V8_ARCHIVE").is_ok()) + || env::var("GN_ARGS").is_ok_and(|gn_args| { + gn_args + .split_whitespace() + .any(|ba| ba == "use_custom_libcxx=false") + }); + + if should_dyn_link_libcxx { + // Based on https://github.com/alexcrichton/cc-rs/blob/fba7feded71ee4f63cfe885673ead6d7b4f2f454/src/lib.rs#L2462 + if let Ok(stdlib) = env::var("CXXSTDLIB") { + if !stdlib.is_empty() { + println!("cargo:rustc-link-lib=dylib={stdlib}"); + } + } else { +- let target = env::var("TARGET").unwrap(); + if target.contains("msvc") { + // nothing to link to + } else if target.contains("apple") diff --git a/patches/v8_bazel_rules.patch b/patches/v8_bazel_rules.patch index 0596ea8396..66aff5e4d2 100644 --- a/patches/v8_bazel_rules.patch +++ b/patches/v8_bazel_rules.patch @@ -121,7 +121,7 @@ index 85f31b7..7314584 100644 ], outs = [ "include/inspector/Debugger.h", -@@ -4426,15 +4426,19 @@ genrule( +@@ -4426,15 +4426,18 @@ genrule( "src/inspector/protocol/Schema.cpp", "src/inspector/protocol/Schema.h", ], @@ -134,7 +134,7 @@ index 85f31b7..7314584 100644 --config $(location :src/inspector/inspector_protocol_config.json) \ --config_value protocol.path=$(location :include/js_protocol.pdl) \ --output_base $(@D)/src/inspector", - local = 1, +- local = 1, message = "Generating inspector files", tools = [ - ":code_generator", diff --git a/third_party/v8/BUILD.bazel b/third_party/v8/BUILD.bazel index cfdbabf468..b32bc2f495 100644 --- a/third_party/v8/BUILD.bazel +++ b/third_party/v8/BUILD.bazel @@ -4,6 +4,158 @@ load("@rules_cc//cc:defs.bzl", "cc_library") package(default_visibility = ["//visibility:public"]) +config_setting( + name = "platform_aarch64_unknown_linux_musl", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + "@llvm//constraints/libc:musl", + ], +) + +config_setting( + name = "platform_x86_64_unknown_linux_musl", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + "@llvm//constraints/libc:musl", + ], +) + +alias( + name = "v8_146_4_0_x86_64_apple_darwin", + actual = "@rusty_v8_146_4_0_x86_64_apple_darwin_archive//file", +) + +alias( + name = "v8_146_4_0_aarch64_apple_darwin", + actual = "@rusty_v8_146_4_0_aarch64_apple_darwin_archive//file", +) + +alias( + name = "v8_146_4_0_x86_64_unknown_linux_gnu", + actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_gnu_archive//file", +) + +alias( + name = "v8_146_4_0_aarch64_unknown_linux_gnu", + actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_gnu_archive//file", +) + +alias( + name = "v8_146_4_0_x86_64_unknown_linux_musl", + actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_musl_archive//file", +) + +alias( + name = "v8_146_4_0_aarch64_unknown_linux_musl", + actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_musl_archive//file", +) + +alias( + name = "v8_146_4_0_x86_64_pc_windows_msvc", + actual = "@rusty_v8_146_4_0_x86_64_pc_windows_msvc_archive//file", +) + +alias( + name = "v8_146_4_0_aarch64_pc_windows_msvc", + actual = "@rusty_v8_146_4_0_aarch64_pc_windows_msvc_archive//file", +) + +alias( + name = "v8_146_4_0_aarch64_pc_windows_gnullvm", + actual = ":v8_146_4_0_aarch64_pc_windows_msvc", +) + +alias( + name = "v8_146_4_0_x86_64_pc_windows_gnullvm", + actual = ":v8_146_4_0_x86_64_pc_windows_msvc", +) + +filegroup( + name = "src_binding_release_x86_64_apple_darwin", + srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_apple_darwin"], +) + +filegroup( + name = "src_binding_release_aarch64_apple_darwin", + srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_apple_darwin"], +) + +filegroup( + name = "src_binding_release_aarch64_unknown_linux_gnu", + srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"], +) + +filegroup( + name = "src_binding_release_x86_64_unknown_linux_gnu", + srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"], +) + +alias( + name = "src_binding_release_x86_64_unknown_linux_musl", + actual = "@rusty_v8_146_4_0_x86_64_unknown_linux_musl_binding//file", +) + +alias( + name = "src_binding_release_aarch64_unknown_linux_musl", + actual = "@rusty_v8_146_4_0_aarch64_unknown_linux_musl_binding//file", +) + +filegroup( + name = "src_binding_release_x86_64_pc_windows_msvc", + srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_pc_windows_msvc"], +) + +filegroup( + name = "src_binding_release_aarch64_pc_windows_msvc", + srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_pc_windows_msvc"], +) + +alias( + name = "src_binding_release_x86_64_pc_windows_gnullvm", + actual = ":src_binding_release_x86_64_pc_windows_msvc", +) + +alias( + name = "src_binding_release_aarch64_pc_windows_gnullvm", + actual = ":src_binding_release_aarch64_pc_windows_msvc", +) + +alias( + name = "rusty_v8_archive_for_target", + actual = select({ + "@rules_rs//rs/experimental/platforms/config:aarch64-apple-darwin": ":v8_146_4_0_aarch64_apple_darwin_bazel", + "@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-gnullvm": ":v8_146_4_0_aarch64_pc_windows_gnullvm", + "@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-msvc": ":v8_146_4_0_aarch64_pc_windows_msvc", + "@rules_rs//rs/experimental/platforms/config:aarch64-unknown-linux-gnu": ":v8_146_4_0_aarch64_unknown_linux_gnu_bazel", + ":platform_aarch64_unknown_linux_musl": ":v8_146_4_0_aarch64_unknown_linux_musl_release_base", + "@rules_rs//rs/experimental/platforms/config:x86_64-apple-darwin": ":v8_146_4_0_x86_64_apple_darwin_bazel", + "@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-gnullvm": ":v8_146_4_0_x86_64_pc_windows_gnullvm", + "@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-msvc": ":v8_146_4_0_x86_64_pc_windows_msvc", + "@rules_rs//rs/experimental/platforms/config:x86_64-unknown-linux-gnu": ":v8_146_4_0_x86_64_unknown_linux_gnu_bazel", + ":platform_x86_64_unknown_linux_musl": ":v8_146_4_0_x86_64_unknown_linux_musl_release", + "//conditions:default": ":v8_146_4_0_x86_64_unknown_linux_gnu_bazel", + }), +) + +alias( + name = "rusty_v8_binding_for_target", + actual = select({ + "@rules_rs//rs/experimental/platforms/config:aarch64-apple-darwin": ":src_binding_release_aarch64_apple_darwin", + "@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-gnullvm": ":src_binding_release_aarch64_pc_windows_gnullvm", + "@rules_rs//rs/experimental/platforms/config:aarch64-pc-windows-msvc": ":src_binding_release_aarch64_pc_windows_msvc", + "@rules_rs//rs/experimental/platforms/config:aarch64-unknown-linux-gnu": ":src_binding_release_aarch64_unknown_linux_gnu", + ":platform_aarch64_unknown_linux_musl": ":src_binding_release_aarch64_unknown_linux_musl", + "@rules_rs//rs/experimental/platforms/config:x86_64-apple-darwin": ":src_binding_release_x86_64_apple_darwin", + "@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-gnullvm": ":src_binding_release_x86_64_pc_windows_gnullvm", + "@rules_rs//rs/experimental/platforms/config:x86_64-pc-windows-msvc": ":src_binding_release_x86_64_pc_windows_msvc", + "@rules_rs//rs/experimental/platforms/config:x86_64-unknown-linux-gnu": ":src_binding_release_x86_64_unknown_linux_gnu", + ":platform_x86_64_unknown_linux_musl": ":src_binding_release_x86_64_unknown_linux_musl", + "//conditions:default": ":src_binding_release_x86_64_unknown_linux_gnu", + }), +) + V8_COPTS = ["-std=c++20"] V8_STATIC_LIBRARY_FEATURES = [ @@ -45,39 +197,39 @@ cc_library( ) cc_static_library( - name = "v8_146_4_0_x86_64_apple_darwin", + name = "v8_146_4_0_aarch64_apple_darwin_bazel", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) cc_static_library( - name = "v8_146_4_0_aarch64_apple_darwin", + name = "v8_146_4_0_aarch64_unknown_linux_gnu_bazel", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) cc_static_library( - name = "v8_146_4_0_aarch64_unknown_linux_gnu", + name = "v8_146_4_0_x86_64_apple_darwin_bazel", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) cc_static_library( - name = "v8_146_4_0_x86_64_unknown_linux_gnu", + name = "v8_146_4_0_x86_64_unknown_linux_gnu_bazel", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) cc_static_library( - name = "v8_146_4_0_aarch64_unknown_linux_musl_base", + name = "v8_146_4_0_aarch64_unknown_linux_musl_release_base", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) genrule( - name = "v8_146_4_0_aarch64_unknown_linux_musl", + name = "v8_146_4_0_aarch64_unknown_linux_musl_release", srcs = [ - ":v8_146_4_0_aarch64_unknown_linux_musl_base", + ":v8_146_4_0_aarch64_unknown_linux_musl_release_base", "@llvm//runtimes/compiler-rt:clang_rt.builtins.static", ], tools = [ @@ -88,7 +240,7 @@ genrule( cmd = """ cat > "$(@D)/merge.mri" <<'EOF' create $@ -addlib $(location :v8_146_4_0_aarch64_unknown_linux_musl_base) +addlib $(location :v8_146_4_0_aarch64_unknown_linux_musl_release_base) addlib $(location @llvm//runtimes/compiler-rt:clang_rt.builtins.static) save end @@ -99,83 +251,21 @@ EOF ) cc_static_library( - name = "v8_146_4_0_x86_64_unknown_linux_musl", + name = "v8_146_4_0_x86_64_unknown_linux_musl_release", deps = [":v8_146_4_0_binding"], features = V8_STATIC_LIBRARY_FEATURES, ) -cc_static_library( - name = "v8_146_4_0_aarch64_pc_windows_msvc", - deps = [":v8_146_4_0_binding"], - features = V8_STATIC_LIBRARY_FEATURES, -) - -cc_static_library( - name = "v8_146_4_0_x86_64_pc_windows_msvc", - deps = [":v8_146_4_0_binding"], - features = V8_STATIC_LIBRARY_FEATURES, -) - -alias( - name = "v8_146_4_0_aarch64_pc_windows_gnullvm", - actual = ":v8_146_4_0_aarch64_pc_windows_msvc", -) - -alias( - name = "v8_146_4_0_x86_64_pc_windows_gnullvm", - actual = ":v8_146_4_0_x86_64_pc_windows_msvc", -) - filegroup( - name = "src_binding_release_x86_64_apple_darwin", - srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_apple_darwin"], -) - -filegroup( - name = "src_binding_release_aarch64_apple_darwin", - srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_apple_darwin"], -) - -filegroup( - name = "src_binding_release_aarch64_unknown_linux_gnu", + name = "src_binding_release_aarch64_unknown_linux_musl_release", srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"], ) filegroup( - name = "src_binding_release_x86_64_unknown_linux_gnu", + name = "src_binding_release_x86_64_unknown_linux_musl_release", srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"], ) -filegroup( - name = "src_binding_release_aarch64_unknown_linux_musl", - srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_unknown_linux_gnu"], -) - -filegroup( - name = "src_binding_release_x86_64_unknown_linux_musl", - srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_unknown_linux_gnu"], -) - -filegroup( - name = "src_binding_release_x86_64_pc_windows_msvc", - srcs = ["@v8_crate_146_4_0//:src_binding_release_x86_64_pc_windows_msvc"], -) - -filegroup( - name = "src_binding_release_aarch64_pc_windows_msvc", - srcs = ["@v8_crate_146_4_0//:src_binding_release_aarch64_pc_windows_msvc"], -) - -alias( - name = "src_binding_release_x86_64_pc_windows_gnullvm", - actual = ":src_binding_release_x86_64_pc_windows_msvc", -) - -alias( - name = "src_binding_release_aarch64_pc_windows_gnullvm", - actual = ":src_binding_release_aarch64_pc_windows_msvc", -) - filegroup( name = "rusty_v8_release_pair_x86_64_apple_darwin", srcs = [ @@ -211,16 +301,16 @@ filegroup( filegroup( name = "rusty_v8_release_pair_x86_64_unknown_linux_musl", srcs = [ - ":v8_146_4_0_x86_64_unknown_linux_musl", - ":src_binding_release_x86_64_unknown_linux_musl", + ":v8_146_4_0_x86_64_unknown_linux_musl_release", + ":src_binding_release_x86_64_unknown_linux_musl_release", ], ) filegroup( name = "rusty_v8_release_pair_aarch64_unknown_linux_musl", srcs = [ - ":v8_146_4_0_aarch64_unknown_linux_musl", - ":src_binding_release_aarch64_unknown_linux_musl", + ":v8_146_4_0_aarch64_unknown_linux_musl_release", + ":src_binding_release_aarch64_unknown_linux_musl_release", ], ) diff --git a/third_party/v8/README.md b/third_party/v8/README.md index 3931bbca46..9ad37c6f08 100644 --- a/third_party/v8/README.md +++ b/third_party/v8/README.md @@ -1,45 +1,47 @@ -# `rusty_v8` Release Artifacts +# `rusty_v8` Consumer Artifacts -This directory contains the Bazel packaging used to build and stage -target-specific `rusty_v8` release artifacts for Bazel-managed consumers. +This directory wires the `v8` crate to exact-version Bazel inputs. +Bazel consumer builds use: + +- upstream `denoland/rusty_v8` release archives on Windows +- source-built V8 archives on Darwin, GNU Linux, and musl Linux +- `openai/codex` release assets for published musl release pairs + +Cargo builds still use prebuilt `rusty_v8` archives by default. Only Bazel +overrides `RUSTY_V8_ARCHIVE`/`RUSTY_V8_SRC_BINDING_PATH` in `MODULE.bazel` to +select source-built local archives for its consumer builds. Current pinned versions: - Rust crate: `v8 = =146.4.0` -- Embedded upstream V8 source: `14.6.202.9` +- Embedded upstream V8 source for musl release builds: `14.6.202.9` -The generated release pairs include: +The consumer-facing selectors are: + +- `//third_party/v8:rusty_v8_archive_for_target` +- `//third_party/v8:rusty_v8_binding_for_target` + +Musl release assets are expected at the tag: + +- `rusty-v8-v` + +with these raw asset names: + +- `librusty_v8_release_.a.gz` +- `src_binding_release_.rs` + +The dedicated publishing workflow is `.github/workflows/rusty-v8-release.yml`. +It builds musl release pairs from source and keeps the release artifacts as the +statically linked form: -- `//third_party/v8:rusty_v8_release_pair_x86_64_apple_darwin` -- `//third_party/v8:rusty_v8_release_pair_aarch64_apple_darwin` -- `//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_gnu` -- `//third_party/v8:rusty_v8_release_pair_aarch64_unknown_linux_gnu` - `//third_party/v8:rusty_v8_release_pair_x86_64_unknown_linux_musl` - `//third_party/v8:rusty_v8_release_pair_aarch64_unknown_linux_musl` -- `//third_party/v8:rusty_v8_release_pair_x86_64_pc_windows_msvc` -- `//third_party/v8:rusty_v8_release_pair_aarch64_pc_windows_msvc` -Each release pair contains: - -- a static library built from source -- a Rust binding file copied from the exact same `v8` crate version for that - target +Cargo musl builds use `RUSTY_V8_ARCHIVE` plus a downloaded +`RUSTY_V8_SRC_BINDING_PATH` to point at those `openai/codex` release assets +directly. We do not use `RUSTY_V8_MIRROR` for musl because the upstream `v8` +crate hardcodes a `v` tag layout, while our musl artifacts are +published under `rusty-v8-v`. Do not mix artifacts across crate versions. The archive and binding must match -the exact pinned `v8` crate version used by this repo. - -The dedicated publishing workflow is: - -- `.github/workflows/rusty-v8-release.yml` - -That workflow currently stages musl artifacts: - -- `librusty_v8_release_x86_64-unknown-linux-musl.a.gz` -- `librusty_v8_release_aarch64-unknown-linux-musl.a.gz` -- `src_binding_release_x86_64-unknown-linux-musl.rs` -- `src_binding_release_aarch64-unknown-linux-musl.rs` - -During musl staging, the produced static archive is merged with the target's -LLVM `libc++` and `libc++abi` static runtime archives. Rust's musl toolchain -already provides the matching `libunwind`, so staging does not bundle a second -copy. +the exact resolved `v8` crate version in `codex-rs/Cargo.lock`.