diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index eda359e2a0cd..d5218899ff86 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1435,7 +1435,7 @@ "rustix_1.1.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(criterion, not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.182\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.171\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"name\":\"libc_errno\",\"optional\":true,\"package\":\"errno\",\"req\":\"^0.3.10\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc_errno\",\"package\":\"errno\",\"req\":\"^0.3.10\"},{\"default_features\":false,\"features\":[\"general\",\"ioctl\",\"no_std\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(any(target_os = \\\"linux\\\", target_os = \\\"android\\\"), any(rustix_use_libc, miri, not(all(target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\")))))))\"},{\"default_features\":false,\"features\":[\"auxvec\",\"general\",\"errno\",\"ioctl\",\"no_std\",\"elf\"],\"name\":\"linux-raw-sys\",\"req\":\"^0.12\",\"target\":\"cfg(all(not(rustix_use_libc), not(miri), target_os = \\\"linux\\\", any(target_endian = \\\"little\\\", any(target_arch = \\\"s390x\\\", target_arch = \\\"powerpc\\\")), any(target_arch = \\\"arm\\\", all(target_arch = \\\"aarch64\\\", target_pointer_width = \\\"64\\\"), target_arch = \\\"riscv64\\\", all(rustix_use_experimental_asm, target_arch = \\\"powerpc\\\"), all(rustix_use_experimental_asm, target_arch = \\\"powerpc64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"s390x\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips32r6\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64\\\"), all(rustix_use_experimental_asm, target_arch = \\\"mips64r6\\\"), target_arch = \\\"x86\\\", all(target_arch = \\\"x86_64\\\", target_pointer_width = \\\"64\\\"))))\"},{\"kind\":\"dev\",\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.20.3\",\"target\":\"cfg(windows)\"},{\"name\":\"rustc-std-workspace-alloc\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5.0\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all-apis\":[\"event\",\"fs\",\"io_uring\",\"mm\",\"mount\",\"net\",\"param\",\"pipe\",\"process\",\"pty\",\"rand\",\"runtime\",\"shm\",\"stdio\",\"system\",\"termios\",\"thread\",\"time\"],\"alloc\":[],\"default\":[\"std\"],\"event\":[],\"fs\":[],\"io_uring\":[\"event\",\"fs\",\"net\",\"thread\",\"linux-raw-sys/io_uring\"],\"linux_4_11\":[],\"linux_5_1\":[\"linux_4_11\"],\"linux_5_11\":[\"linux_5_1\"],\"linux_latest\":[\"linux_5_11\"],\"mm\":[],\"mount\":[],\"net\":[\"linux-raw-sys/net\",\"linux-raw-sys/netlink\",\"linux-raw-sys/if_ether\",\"linux-raw-sys/xdp\"],\"param\":[],\"pipe\":[],\"process\":[\"linux-raw-sys/prctl\"],\"pty\":[\"fs\"],\"rand\":[],\"runtime\":[\"linux-raw-sys/prctl\"],\"rustc-dep-of-std\":[\"core\",\"rustc-std-workspace-alloc\",\"linux-raw-sys/rustc-dep-of-std\",\"bitflags/rustc-dep-of-std\"],\"shm\":[\"fs\"],\"std\":[\"bitflags/std\",\"alloc\",\"libc?/std\",\"libc_errno?/std\"],\"stdio\":[],\"system\":[\"linux-raw-sys/system\"],\"termios\":[],\"thread\":[\"linux-raw-sys/prctl\"],\"time\":[],\"try_close\":[],\"use-explicitly-provided-auxv\":[],\"use-libc\":[\"libc_errno\",\"libc\"],\"use-libc-auxv\":[]}}", "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", - "rustls-webpki_0.103.12": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18.1\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", + "rustls-webpki_0.103.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18.1\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", "rustversion_1.0.22": "{\"dependencies\":[{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.49\"}],\"features\":{}}", "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\"]}}", diff --git a/codex-rs/.cargo/audit.toml b/codex-rs/.cargo/audit.toml index 3760d286482a..4d9e4b81eda5 100644 --- a/codex-rs/.cargo/audit.toml +++ b/codex-rs/.cargo/audit.toml @@ -6,5 +6,4 @@ ignore = [ "RUSTSEC-2024-0436", # paste 1.0.15 via starlark/ratatui; upstream crate is unmaintained "RUSTSEC-2024-0320", # yaml-rust via syntect; remove when syntect drops or updates it "RUSTSEC-2025-0141", # bincode via syntect; remove when syntect drops or updates it - "RUSTSEC-2026-0097", # rand 0.8.5 via age/codex-secrets and zbus/keyring; remove when transitive deps move to rand >=0.9.3 ] diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 980f3b87a01e..eec7701bc22e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -10761,9 +10761,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", diff --git a/codex-rs/codex-api/src/files.rs b/codex-rs/codex-api/src/files.rs index d1e2840066d8..99e1c3d3c71d 100644 --- a/codex-rs/codex-api/src/files.rs +++ b/codex-rs/codex-api/src/files.rs @@ -10,6 +10,7 @@ use serde::Deserialize; use tokio::fs::File; use tokio::time::Instant; use tokio_util::io::ReaderStream; +use url::Url; pub const OPENAI_FILE_URI_PREFIX: &str = "sediment://"; pub const OPENAI_FILE_UPLOAD_LIMIT_BYTES: u64 = 512 * 1024 * 1024; @@ -19,11 +20,16 @@ const OPENAI_FILE_FINALIZE_TIMEOUT: Duration = Duration::from_secs(30); const OPENAI_FILE_FINALIZE_RETRY_DELAY: Duration = Duration::from_millis(250); const OPENAI_FILE_USE_CASE: &str = "codex"; +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct OpenAiFileUploadOptions { + pub store_in_library: bool, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct UploadedOpenAiFile { pub file_id: String, pub uri: String, - pub download_url: String, + pub download_url: Option, pub file_name: String, pub file_size_bytes: u64, pub mime_type: Option, @@ -68,6 +74,12 @@ pub enum OpenAiFileError { #[source] source: serde_json::Error, }, + #[error("failed to resolve OpenAI file URL `{url}`: {source}")] + InvalidUrl { + url: String, + #[source] + source: url::ParseError, + }, #[error("OpenAI file upload for `{file_id}` is not ready yet")] UploadNotReady { file_id: String }, #[error("OpenAI file upload for `{file_id}` failed: {message}")] @@ -94,10 +106,100 @@ pub fn openai_file_uri(file_id: &str) -> String { format!("{OPENAI_FILE_URI_PREFIX}{file_id}") } +fn openai_file_api_base_url(base_url: &str) -> String { + base_url.trim_end_matches('/').to_string() +} + +fn is_localhost_url(url: &Url) -> bool { + matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "::1")) +} + +fn local_dev_same_origin_api_base_url(base_url: &str) -> Option { + let mut url = Url::parse(base_url).ok()?; + if !is_localhost_url(&url) || !matches!(url.path(), "" | "/") { + return None; + } + url.set_path("/api"); + url.set_query(None); + url.set_fragment(None); + Some(url.to_string().trim_end_matches('/').to_string()) +} + +fn local_dev_sa_server_api_base_url(base_url: &str) -> Option { + let mut url = Url::parse(base_url).ok()?; + match url.host_str()? { + "localhost" | "127.0.0.1" | "::1" => {} + _ => return None, + } + if url.port_or_known_default() == Some(8000) { + return None; + } + url.set_port(Some(8000)).ok()?; + url.set_path("/api"); + url.set_query(None); + url.set_fragment(None); + Some(url.to_string().trim_end_matches('/').to_string()) +} + +fn openai_file_api_base_url_candidates(base_url: &str) -> Vec { + let mut candidates = vec![openai_file_api_base_url(base_url)]; + if let Some(candidate) = local_dev_same_origin_api_base_url(base_url) + && !candidates.contains(&candidate) + { + candidates.push(candidate); + } + if let Some(candidate) = local_dev_sa_server_api_base_url(base_url) + && !candidates.contains(&candidate) + { + candidates.push(candidate); + } + candidates +} + +pub async fn download_openai_file( + base_url: &str, + auth: &impl AuthProvider, + download_url: &str, +) -> Result, OpenAiFileError> { + let resolved_url = resolve_openai_file_download_url(base_url, download_url)?; + let request_builder = if should_attach_auth_to_openai_file_url(&resolved_url, base_url) { + authorized_request(auth, reqwest::Method::GET, resolved_url.as_str()) + } else { + build_reqwest_client() + .request(reqwest::Method::GET, resolved_url.as_str()) + .timeout(OPENAI_FILE_REQUEST_TIMEOUT) + }; + let response = request_builder + .send() + .await + .map_err(|source| OpenAiFileError::Request { + url: resolved_url.to_string(), + source, + })?; + let status = response.status(); + if !status.is_success() { + let body = response.text().await.unwrap_or_default(); + return Err(OpenAiFileError::UnexpectedStatus { + url: resolved_url.to_string(), + status, + body, + }); + } + let bytes = response + .bytes() + .await + .map_err(|source| OpenAiFileError::Request { + url: resolved_url.to_string(), + source, + })?; + Ok(bytes.to_vec()) +} + pub async fn upload_local_file( base_url: &str, auth: &dyn AuthProvider, path: &Path, + options: &OpenAiFileUploadOptions, ) -> Result { let metadata = tokio::fs::metadata(path) .await @@ -128,33 +230,40 @@ pub async fn upload_local_file( .and_then(|value| value.to_str()) .unwrap_or("file") .to_string(); - let create_url = format!("{}/files", base_url.trim_end_matches('/')); - let create_response = authorized_request(auth, reqwest::Method::POST, &create_url) - .json(&serde_json::json!({ - "file_name": file_name, - "file_size": metadata.len(), - "use_case": OPENAI_FILE_USE_CASE, - })) - .send() - .await - .map_err(|source| OpenAiFileError::Request { - url: create_url.clone(), - source, - })?; - let create_status = create_response.status(); - let create_body = create_response.text().await.unwrap_or_default(); - if !create_status.is_success() { - return Err(OpenAiFileError::UnexpectedStatus { - url: create_url, - status: create_status, - body: create_body, - }); + let mut create_request = serde_json::json!({ + "file_name": file_name, + "file_size": metadata.len(), + "use_case": OPENAI_FILE_USE_CASE, + }); + if options.store_in_library { + create_request["store_in_library"] = serde_json::json!(true); } - let create_payload: CreateFileResponse = - serde_json::from_str(&create_body).map_err(|source| OpenAiFileError::Decode { - url: create_url.clone(), - source, - })?; + let mut last_not_found_error = None; + let mut create_payload = None; + let mut api_base_url = openai_file_api_base_url(base_url); + for candidate in openai_file_api_base_url_candidates(base_url) { + match create_file(auth, &candidate, &create_request).await { + Ok(payload) => { + api_base_url = candidate; + create_payload = Some(payload); + break; + } + Err(error @ OpenAiFileError::UnexpectedStatus { status, .. }) + if status == StatusCode::NOT_FOUND => + { + last_not_found_error = Some(error); + } + Err(error) => return Err(error), + } + } + let Some(create_payload) = create_payload else { + return Err( + last_not_found_error.unwrap_or(OpenAiFileError::UploadFailed { + file_id: "unknown".to_string(), + message: "file creation did not produce a response".to_string(), + }), + ); + }; let upload_file = File::open(path) .await @@ -184,11 +293,19 @@ pub async fn upload_local_file( }); } - let finalize_url = format!( - "{}/files/{}/uploaded", - base_url.trim_end_matches('/'), - create_payload.file_id, - ); + if options.store_in_library { + return Ok(UploadedOpenAiFile { + file_id: create_payload.file_id.clone(), + uri: openai_file_uri(&create_payload.file_id), + download_url: None, + file_name, + file_size_bytes: metadata.len(), + mime_type: None, + path: path.to_path_buf(), + }); + } + + let finalize_url = format!("{api_base_url}/files/{}/uploaded", create_payload.file_id); let finalize_started_at = Instant::now(); loop { let finalize_response = authorized_request(auth, reqwest::Method::POST, &finalize_url) @@ -219,12 +336,12 @@ pub async fn upload_local_file( return Ok(UploadedOpenAiFile { file_id: create_payload.file_id.clone(), uri: openai_file_uri(&create_payload.file_id), - download_url: finalize_payload.download_url.ok_or_else(|| { + download_url: Some(finalize_payload.download_url.ok_or_else(|| { OpenAiFileError::UploadFailed { file_id: create_payload.file_id.clone(), message: "missing download_url".to_string(), } - })?, + })?), file_name: finalize_payload.file_name.unwrap_or(file_name), file_size_bytes: metadata.len(), mime_type: finalize_payload.mime_type, @@ -251,6 +368,35 @@ pub async fn upload_local_file( } } +async fn create_file( + auth: &dyn AuthProvider, + api_base_url: &str, + create_request: &serde_json::Value, +) -> Result { + let create_url = format!("{api_base_url}/files"); + let create_response = authorized_request(auth, reqwest::Method::POST, &create_url) + .json(create_request) + .send() + .await + .map_err(|source| OpenAiFileError::Request { + url: create_url.clone(), + source, + })?; + let create_status = create_response.status(); + let create_body = create_response.text().await.unwrap_or_default(); + if !create_status.is_success() { + return Err(OpenAiFileError::UnexpectedStatus { + url: create_url, + status: create_status, + body: create_body, + }); + } + serde_json::from_str(&create_body).map_err(|source| OpenAiFileError::Decode { + url: create_url, + source, + }) +} + fn authorized_request( auth: &dyn AuthProvider, method: reqwest::Method, @@ -266,6 +412,46 @@ fn authorized_request( .headers(headers) } +fn resolve_openai_file_download_url( + base_url: &str, + download_url: &str, +) -> Result { + match Url::parse(download_url) { + Ok(url) => Ok(url), + Err(url::ParseError::RelativeUrlWithoutBase) => { + let normalized_base_url = if base_url.ends_with('/') { + base_url.to_string() + } else { + format!("{base_url}/") + }; + let base = + Url::parse(&normalized_base_url).map_err(|source| OpenAiFileError::InvalidUrl { + url: normalized_base_url.clone(), + source, + })?; + base.join(download_url) + .map_err(|source| OpenAiFileError::InvalidUrl { + url: download_url.to_string(), + source, + }) + } + Err(source) => Err(OpenAiFileError::InvalidUrl { + url: download_url.to_string(), + source, + }), + } +} + +fn should_attach_auth_to_openai_file_url(download_url: &Url, base_url: &str) -> bool { + let Ok(base_url) = Url::parse(base_url) else { + return false; + }; + match (download_url.host_str(), base_url.host_str()) { + (Some(download_host), Some(base_host)) => download_host.eq_ignore_ascii_case(base_host), + _ => false, + } +} + fn build_reqwest_client() -> reqwest::Client { build_reqwest_client_with_custom_ca(reqwest::Client::builder()).unwrap_or_else(|error| { tracing::warn!(error = %error, "failed to build OpenAI file upload client"); @@ -312,6 +498,32 @@ mod tests { format!("{}/backend-api", server.uri()) } + #[tokio::test] + async fn download_openai_file_resolves_relative_url_and_attaches_auth() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/files/download/file_123")) + .and(header("authorization", "Bearer token")) + .and(header("chatgpt-account-id", "account_id")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "text/plain") + .set_body_bytes(b"hello".to_vec()), + ) + .mount(&server) + .await; + + let downloaded = download_openai_file( + &format!("{}/backend-api/codex", server.uri()), + &chatgpt_auth(), + "/files/download/file_123", + ) + .await + .expect("download succeeds"); + + assert_eq!(downloaded, b"hello".to_vec()); + } + #[tokio::test] async fn upload_local_file_returns_canonical_uri() { let server = MockServer::start().await; @@ -363,15 +575,20 @@ mod tests { let path = dir.path().join("hello.txt"); tokio::fs::write(&path, b"hello").await.expect("write file"); - let uploaded = upload_local_file(&base_url, &chatgpt_auth(), &path) - .await - .expect("upload succeeds"); + let uploaded = upload_local_file( + &base_url, + &chatgpt_auth(), + &path, + &OpenAiFileUploadOptions::default(), + ) + .await + .expect("upload succeeds"); assert_eq!(uploaded.file_id, "file_123"); assert_eq!(uploaded.uri, "sediment://file_123"); assert_eq!( uploaded.download_url, - format!("{}/download/file_123", server.uri()) + Some(format!("{}/download/file_123", server.uri())) ); assert_eq!(uploaded.file_name, "hello.txt"); assert_eq!(uploaded.mime_type, Some("text/plain".to_string())); diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index 0b8aee266b0b..f1eb23a96e25 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -57,6 +57,8 @@ pub use crate::endpoint::ResponsesWebsocketClient; pub use crate::endpoint::ResponsesWebsocketConnection; pub use crate::endpoint::session_update_session_json; pub use crate::error::ApiError; +pub use crate::files::OpenAiFileUploadOptions; +pub use crate::files::download_openai_file; pub use crate::files::upload_local_file; pub use crate::provider::Provider; pub use crate::provider::RetryConfig; diff --git a/codex-rs/core/src/codex_apps_file_download.rs b/codex-rs/core/src/codex_apps_file_download.rs new file mode 100644 index 000000000000..711ef4d19a70 --- /dev/null +++ b/codex-rs/core/src/codex_apps_file_download.rs @@ -0,0 +1,374 @@ +use crate::codex_apps_mcp_tools::should_materialize_codex_apps_file_download; +use crate::session::session::Session; +use crate::session::turn_context::TurnContext; +use codex_api::download_openai_file; +use codex_login::CodexAuth; +use codex_model_provider::BearerAuthProvider; +use codex_protocol::mcp::CallToolResult; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Map as JsonMap; +use serde_json::Value as JsonValue; +use tracing::warn; + +const CODEX_APPS_FILE_DOWNLOAD_ARTIFACTS_DIR: &str = ".tmp/codex_apps_downloads"; + +#[derive(Debug, Deserialize, Serialize)] +struct CodexAppsFileDownloadPayload { + file_id: String, + #[serde(default)] + file_name: Option, + file_uri: CodexAppsFileUri, +} + +#[derive(Debug, Deserialize, Serialize)] +struct CodexAppsFileUri { + download_url: String, + #[serde(default)] + file_name: Option, +} + +fn codex_apps_download_base_url(turn_context: &TurnContext) -> &str { + turn_context.config.chatgpt_base_url.as_str() +} + +pub(crate) async fn maybe_materialize_codex_apps_file_download_result( + sess: &Session, + turn_context: &TurnContext, + server: &str, + codex_apps_meta: Option<&JsonMap>, + result: CallToolResult, +) -> CallToolResult { + let auth = sess.services.auth_manager.auth().await; + maybe_materialize_codex_apps_file_download_result_with_auth( + turn_context, + &sess.conversation_id.to_string(), + auth.as_ref(), + server, + codex_apps_meta, + result, + ) + .await +} + +async fn maybe_materialize_codex_apps_file_download_result_with_auth( + turn_context: &TurnContext, + session_id: &str, + auth: Option<&CodexAuth>, + server: &str, + codex_apps_meta: Option<&JsonMap>, + mut result: CallToolResult, +) -> CallToolResult { + if !should_materialize_codex_apps_file_download(server, codex_apps_meta) + || result.is_error == Some(true) + { + return result; + } + + let Some(payload) = extract_codex_apps_file_download_payload(&result) else { + return result; + }; + let download_base_url = codex_apps_download_base_url(turn_context); + if result.structured_content.is_none() + && let Ok(structured_content) = serde_json::to_value(&payload) + { + result.structured_content = Some(structured_content); + } + + let Some(auth) = auth else { + warn!( + "skipping codex_apps file download materialization because ChatGPT auth is unavailable" + ); + return result; + }; + let token_data = match auth.get_token_data() { + Ok(token_data) => token_data, + Err(error) => { + warn!(error = %error, "failed to read ChatGPT auth for codex_apps file download materialization"); + return result; + } + }; + let auth_provider = BearerAuthProvider { + token: Some(token_data.access_token), + account_id: token_data.account_id, + is_fedramp_account: auth.is_fedramp_account(), + }; + let downloaded = match download_openai_file( + download_base_url, + &auth_provider, + &payload.file_uri.download_url, + ) + .await + { + Ok(downloaded) => downloaded, + Err(error) => { + warn!( + error = %error, + file_id = payload.file_id, + "failed to materialize codex_apps file download via app-server", + ); + return result; + } + }; + + let artifact_path = codex_apps_file_download_artifact_path( + &turn_context.config.codex_home, + session_id, + &payload.file_id, + payload + .file_name + .as_deref() + .or(payload.file_uri.file_name.as_deref()) + .unwrap_or("downloaded_file"), + ); + if let Some(parent) = artifact_path.parent() + && let Err(error) = tokio::fs::create_dir_all(parent.as_path()).await + { + warn!( + error = %error, + path = %parent.display(), + "failed to create codex_apps file download artifact directory", + ); + return result; + } + if let Err(error) = tokio::fs::write(artifact_path.as_path(), &downloaded).await { + warn!( + error = %error, + path = %artifact_path.display(), + "failed to write codex_apps file download artifact", + ); + return result; + } + + let local_path = artifact_path.to_string_lossy().to_string(); + if let Some(JsonValue::Object(map)) = result.structured_content.as_mut() { + map.insert( + "local_path".to_string(), + JsonValue::String(local_path.clone()), + ); + } + result.content.push(serde_json::json!({ + "type": "text", + "text": format!("Downloaded file to local path: {local_path}"), + })); + result +} + +fn extract_codex_apps_file_download_payload( + result: &CallToolResult, +) -> Option { + if let Some(structured_content) = result.structured_content.clone() + && let Ok(payload) = + serde_json::from_value::(structured_content) + { + return Some(payload); + } + + result + .content + .iter() + .filter_map(|item| item.as_object()) + .find_map(|item| { + let text = item.get("text")?.as_str()?; + serde_json::from_str::(text).ok() + }) +} + +fn codex_apps_file_download_artifact_path( + codex_home: &codex_utils_absolute_path::AbsolutePathBuf, + session_id: &str, + file_id: &str, + file_name: &str, +) -> codex_utils_absolute_path::AbsolutePathBuf { + codex_home + .join(CODEX_APPS_FILE_DOWNLOAD_ARTIFACTS_DIR) + .join(sanitize_path_component(session_id, "session")) + .join(sanitize_path_component(file_id, "file")) + .join(sanitize_file_name(file_name)) +} + +fn sanitize_path_component(value: &str, fallback: &str) -> String { + let sanitized: String = value + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '_' + } + }) + .collect(); + if sanitized.is_empty() { + fallback.to_string() + } else { + sanitized + } +} + +fn sanitize_file_name(value: &str) -> String { + let sanitized: String = value + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' { + ch + } else { + '_' + } + }) + .collect(); + if sanitized.is_empty() { + "downloaded_file".to_string() + } else { + sanitized + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::session::tests::make_session_and_context; + use codex_login::CodexAuth; + use pretty_assertions::assert_eq; + use std::sync::Arc; + use wiremock::Mock; + use wiremock::MockServer; + use wiremock::ResponseTemplate; + use wiremock::matchers::header; + use wiremock::matchers::method; + use wiremock::matchers::path; + + fn download_materialization_meta() -> JsonMap { + serde_json::json!({ + "provider": "builtin", + "materialize_file_download": true, + }) + .as_object() + .cloned() + .expect("_codex_apps metadata object") + } + + #[tokio::test] + async fn codex_apps_file_download_materialization_adds_local_path_for_marked_tools() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/download/file_123")) + .and(header("authorization", "Bearer Access Token")) + .and(header("chatgpt-account-id", "account_id")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "text/plain") + .set_body_bytes(b"downloaded contents".to_vec()), + ) + .mount(&server) + .await; + + let (_, mut turn_context) = make_session_and_context().await; + let mut config = (*turn_context.config).clone(); + config.chatgpt_base_url = format!("{}/backend-api/codex", server.uri()); + turn_context.config = Arc::new(config); + let original = CallToolResult { + content: vec![serde_json::json!({ + "type": "text", + "text": "{\"file_id\":\"file_123\"}", + })], + structured_content: Some(serde_json::json!({ + "file_id": "file_123", + "file_name": "testing-file.txt", + "file_uri": { + "download_url": format!("{}/download/file_123", server.uri()), + "file_id": "file_123", + "file_name": "testing-file.txt", + "mime_type": "text/plain", + } + })), + is_error: Some(false), + meta: None, + }; + + let result = maybe_materialize_codex_apps_file_download_result_with_auth( + &turn_context, + "session-1", + Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()), + codex_mcp::CODEX_APPS_MCP_SERVER_NAME, + Some(&download_materialization_meta()), + original, + ) + .await; + + let local_path = result + .structured_content + .as_ref() + .and_then(|value| value.get("local_path")) + .and_then(JsonValue::as_str) + .expect("local_path in structured content"); + assert!(local_path.contains("codex_apps_downloads")); + let saved = tokio::fs::read(local_path) + .await + .expect("saved local file should exist"); + assert_eq!(saved, b"downloaded contents".to_vec()); + assert!(result.content.iter().any(|block| { + block.get("type").and_then(JsonValue::as_str) == Some("text") + && block + .get("text") + .and_then(JsonValue::as_str) + .is_some_and(|text| text.contains("Downloaded file to local path:")) + })); + } + + #[tokio::test] + async fn codex_apps_file_download_materialization_uses_chatgpt_base_for_relative_codex_urls() { + let chatgpt_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/api/codex/files/file_123/content")) + .and(header("authorization", "Bearer Access Token")) + .and(header("chatgpt-account-id", "account_id")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "text/plain") + .set_body_bytes(b"downloaded via codex backend".to_vec()), + ) + .mount(&chatgpt_server) + .await; + + let (_, mut turn_context) = make_session_and_context().await; + let mut config = (*turn_context.config).clone(); + config.chatgpt_base_url = chatgpt_server.uri(); + turn_context.config = Arc::new(config); + let original = CallToolResult { + content: vec![], + structured_content: Some(serde_json::json!({ + "file_id": "file_123", + "file_name": "testing-file.txt", + "file_uri": { + "download_url": "/api/codex/files/file_123/content", + "file_id": "file_123", + "file_name": "testing-file.txt", + "mime_type": "text/plain", + } + })), + is_error: Some(false), + meta: None, + }; + + let result = maybe_materialize_codex_apps_file_download_result_with_auth( + &turn_context, + "session-1", + Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()), + codex_mcp::CODEX_APPS_MCP_SERVER_NAME, + Some(&download_materialization_meta()), + original, + ) + .await; + + let local_path = result + .structured_content + .as_ref() + .and_then(|value| value.get("local_path")) + .and_then(JsonValue::as_str) + .expect("local_path in structured content"); + assert_eq!( + tokio::fs::read(local_path).await.expect("downloaded file"), + b"downloaded via codex backend" + ); + } +} diff --git a/codex-rs/core/src/codex_apps_mcp_tools.rs b/codex-rs/core/src/codex_apps_mcp_tools.rs new file mode 100644 index 000000000000..e2c47788877b --- /dev/null +++ b/codex-rs/core/src/codex_apps_mcp_tools.rs @@ -0,0 +1,72 @@ +use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; +use codex_mcp::ToolInfo as McpToolInfo; +use serde_json::Map; +use serde_json::Value; + +pub(crate) const CODEX_APPS_META_KEY: &str = "_codex_apps"; + +const CODEX_APPS_PROVIDER_BUILTIN: &str = "builtin"; +const CODEX_APPS_META_PROVIDER_KEY: &str = "provider"; +const CODEX_APPS_META_DIRECT_EXPOSE_KEY: &str = "direct_expose"; +const CODEX_APPS_META_MATERIALIZE_FILE_DOWNLOAD_KEY: &str = "materialize_file_download"; + +pub(crate) fn is_direct_exposed_codex_apps_builtin_tool_info(tool: &McpToolInfo) -> bool { + is_direct_exposed_codex_apps_builtin( + &tool.server_name, + tool.connector_id.as_deref(), + codex_apps_meta_from_tool_info(tool), + ) +} + +pub(crate) fn is_direct_exposed_codex_apps_builtin( + server_name: &str, + connector_id: Option<&str>, + codex_apps_meta: Option<&Map>, +) -> bool { + if server_name != CODEX_APPS_MCP_SERVER_NAME || connector_id.is_some() { + return false; + } + + let Some(codex_apps_meta) = codex_apps_meta else { + return false; + }; + + codex_apps_meta + .get(CODEX_APPS_META_PROVIDER_KEY) + .and_then(Value::as_str) + == Some(CODEX_APPS_PROVIDER_BUILTIN) + && codex_apps_meta + .get(CODEX_APPS_META_DIRECT_EXPOSE_KEY) + .and_then(Value::as_bool) + == Some(true) +} + +pub(crate) fn should_materialize_codex_apps_file_download( + server_name: &str, + codex_apps_meta: Option<&Map>, +) -> bool { + if server_name != CODEX_APPS_MCP_SERVER_NAME { + return false; + } + + let Some(codex_apps_meta) = codex_apps_meta else { + return false; + }; + + codex_apps_meta + .get(CODEX_APPS_META_PROVIDER_KEY) + .and_then(Value::as_str) + == Some(CODEX_APPS_PROVIDER_BUILTIN) + && codex_apps_meta + .get(CODEX_APPS_META_MATERIALIZE_FILE_DOWNLOAD_KEY) + .and_then(Value::as_bool) + == Some(true) +} + +fn codex_apps_meta_from_tool_info(tool: &McpToolInfo) -> Option<&Map> { + tool.tool + .meta + .as_ref() + .and_then(|meta| meta.get(CODEX_APPS_META_KEY)) + .and_then(Value::as_object) +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 373e57737be0..00a0e00a318d 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -20,6 +20,8 @@ mod compact_remote; pub use codex_thread::CodexThread; pub use codex_thread::ThreadConfigSnapshot; mod agent; +mod codex_apps_file_download; +mod codex_apps_mcp_tools; mod codex_delegate; mod command_canonicalization; mod commit_attribution; diff --git a/codex-rs/core/src/mcp_openai_file.rs b/codex-rs/core/src/mcp_openai_file.rs index d6e6d1f9c072..50e006dfebca 100644 --- a/codex-rs/core/src/mcp_openai_file.rs +++ b/codex-rs/core/src/mcp_openai_file.rs @@ -12,6 +12,7 @@ use crate::session::session::Session; use crate::session::turn_context::TurnContext; +use codex_api::OpenAiFileUploadOptions; use codex_api::upload_local_file; use codex_login::CodexAuth; use codex_model_provider::BearerAuthProvider; @@ -22,6 +23,7 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( turn_context: &TurnContext, arguments_value: Option, openai_file_input_params: Option<&[String]>, + upload_options: Option<&OpenAiFileUploadOptions>, ) -> Result, String> { let Some(openai_file_input_params) = openai_file_input_params else { return Ok(arguments_value); @@ -40,9 +42,14 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files( let Some(value) = arguments.get(field_name) else { continue; }; - let Some(uploaded_value) = - rewrite_argument_value_for_openai_files(turn_context, auth.as_ref(), field_name, value) - .await? + let Some(uploaded_value) = rewrite_argument_value_for_openai_files( + turn_context, + auth.as_ref(), + field_name, + value, + upload_options, + ) + .await? else { continue; }; @@ -61,6 +68,7 @@ async fn rewrite_argument_value_for_openai_files( auth: Option<&CodexAuth>, field_name: &str, value: &JsonValue, + upload_options: Option<&OpenAiFileUploadOptions>, ) -> Result, String> { match value { JsonValue::String(path_or_file_ref) => { @@ -70,6 +78,7 @@ async fn rewrite_argument_value_for_openai_files( field_name, /*index*/ None, path_or_file_ref, + upload_options, ) .await?; Ok(Some(rewritten)) @@ -86,6 +95,7 @@ async fn rewrite_argument_value_for_openai_files( field_name, Some(index), path_or_file_ref, + upload_options, ) .await?; rewritten_values.push(rewritten); @@ -102,6 +112,7 @@ async fn build_uploaded_local_argument_value( field_name: &str, index: Option, file_path: &str, + upload_options: Option<&OpenAiFileUploadOptions>, ) -> Result { let resolved_path = turn_context.resolve_path(Some(file_path.to_string())); let Some(auth) = auth else { @@ -117,10 +128,12 @@ async fn build_uploaded_local_argument_value( account_id: token_data.account_id, is_fedramp_account: auth.is_fedramp_account(), }; + let default_upload_options = OpenAiFileUploadOptions::default(); let uploaded = upload_local_file( turn_context.config.chatgpt_base_url.trim_end_matches('/'), &upload_auth, &resolved_path, + upload_options.unwrap_or(&default_upload_options), ) .await .map_err(|error| match index { @@ -129,14 +142,25 @@ async fn build_uploaded_local_argument_value( } None => format!("failed to upload `{file_path}` for `{field_name}`: {error}"), })?; - Ok(serde_json::json!({ - "download_url": uploaded.download_url, - "file_id": uploaded.file_id, - "mime_type": uploaded.mime_type, - "file_name": uploaded.file_name, - "uri": uploaded.uri, - "file_size_bytes": uploaded.file_size_bytes, - })) + let mut uploaded_value = serde_json::Map::from_iter([ + ("file_id".to_string(), serde_json::json!(uploaded.file_id)), + ( + "file_name".to_string(), + serde_json::json!(uploaded.file_name), + ), + ("uri".to_string(), serde_json::json!(uploaded.uri)), + ( + "file_size_bytes".to_string(), + serde_json::json!(uploaded.file_size_bytes), + ), + ]); + if let Some(download_url) = uploaded.download_url { + uploaded_value.insert("download_url".to_string(), serde_json::json!(download_url)); + } + if let Some(mime_type) = uploaded.mime_type { + uploaded_value.insert("mime_type".to_string(), serde_json::json!(mime_type)); + } + Ok(JsonValue::Object(uploaded_value)) } #[cfg(test)] @@ -160,6 +184,7 @@ mod tests { &Arc::new(turn_context), arguments.clone(), /*openai_file_input_params*/ None, + /*upload_options*/ None, ) .await .expect("rewrite should succeed"); @@ -231,6 +256,7 @@ mod tests { "file", /*index*/ None, "file_report.csv", + /*upload_options*/ None, ) .await .expect("rewrite should upload the local file"); @@ -310,6 +336,7 @@ mod tests { Some(&auth), "file", &serde_json::json!("file_report.csv"), + /*upload_options*/ None, ) .await .expect("rewrite should succeed"); @@ -424,6 +451,7 @@ mod tests { Some(&auth), "files", &serde_json::json!(["one.csv", "two.csv"]), + /*upload_options*/ None, ) .await .expect("rewrite should succeed"); @@ -464,6 +492,7 @@ mod tests { "file": "/definitely/missing/file.csv", })), Some(&["file".to_string()]), + /*upload_options*/ None, ) .await .expect_err("missing file should fail"); diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 82bbc7ca4a4d..52147f0043a0 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -12,6 +12,9 @@ use tracing::error; use crate::arc_monitor::ArcMonitorOutcome; use crate::arc_monitor::monitor_action; +use crate::codex_apps_file_download::maybe_materialize_codex_apps_file_download_result; +use crate::codex_apps_mcp_tools::CODEX_APPS_META_KEY; +use crate::codex_apps_mcp_tools::is_direct_exposed_codex_apps_builtin; use crate::config::Config; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; @@ -33,6 +36,7 @@ use crate::session::turn_context::TurnContext; use codex_analytics::AppInvocation; use codex_analytics::InvocationType; use codex_analytics::build_track_events_context; +use codex_api::OpenAiFileUploadOptions; use codex_config::types::AppToolApproval; use codex_features::Feature; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; @@ -475,6 +479,7 @@ async fn execute_mcp_tool_call( turn_context, arguments_value, metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()), + metadata.and_then(|metadata| metadata.openai_file_upload_options.as_ref()), ) .await?; let request_meta = @@ -487,6 +492,14 @@ async fn execute_mcp_tool_call( .call_tool(server, tool_name, rewritten_arguments, request_meta) .await .map_err(|e| format!("tool call error: {e:?}"))?; + let result = maybe_materialize_codex_apps_file_download_result( + sess, + turn_context, + server, + metadata.and_then(|metadata| metadata.codex_apps_meta.as_ref()), + result, + ) + .await; sanitize_mcp_tool_result_for_model( turn_context .model_info @@ -657,12 +670,36 @@ pub(crate) struct McpToolApprovalMetadata { mcp_app_resource_uri: Option, codex_apps_meta: Option>, openai_file_input_params: Option>, + openai_file_upload_options: Option, } -const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps"; const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate"; const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri"; const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId"; +const MCP_TOOL_OPENAI_FILE_UPLOAD_CONFIG_KEY: &str = "openai/fileUploadConfig"; + +#[derive(Debug, Clone, Deserialize)] +struct RawOpenAiFileUploadConfig { + #[serde(default)] + store_in_library: bool, +} + +fn parse_openai_file_upload_options( + meta: Option<&serde_json::Map>, +) -> Option { + let raw = meta? + .get(MCP_TOOL_OPENAI_FILE_UPLOAD_CONFIG_KEY) + .cloned() + .and_then(|value| serde_json::from_value::(value).ok())?; + + if !raw.store_in_library { + return None; + } + + Some(OpenAiFileUploadOptions { + store_in_library: true, + }) +} fn custom_mcp_tool_approval_mode( turn_context: &TurnContext, @@ -709,7 +746,7 @@ fn build_mcp_tool_call_request_meta( metadata.and_then(|metadata| metadata.codex_apps_meta.clone()) { request_meta.insert( - MCP_TOOL_CODEX_APPS_META_KEY.to_string(), + CODEX_APPS_META_KEY.to_string(), serde_json::Value::Object(codex_apps_meta), ); } @@ -1022,7 +1059,11 @@ fn session_mcp_tool_approval_key( } let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone()); - if invocation.server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() { + if is_direct_exposed_codex_apps_builtin( + invocation.server.as_str(), + connector_id.as_deref(), + metadata.and_then(|metadata| metadata.codex_apps_meta.as_ref()), + ) { return None; } @@ -1154,13 +1195,16 @@ pub(crate) async fn lookup_mcp_tool_metadata( .tool .meta .as_ref() - .and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY)) + .and_then(|meta| meta.get(CODEX_APPS_META_KEY)) .and_then(serde_json::Value::as_object) .cloned(), openai_file_input_params: Some(declared_openai_file_input_param_names( tool_info.tool.meta.as_deref(), )) .filter(|params| !params.is_empty()), + openai_file_upload_options: parse_openai_file_upload_options( + tool_info.tool.meta.as_deref(), + ), }) } diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index d20f2339510e..8b8df553fd8a 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -63,6 +63,7 @@ fn approval_metadata( mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, } } @@ -105,6 +106,24 @@ fn mcp_app_resource_uri_reads_known_tool_meta_keys() { ); } +#[test] +fn parse_openai_file_upload_options_reads_store_in_library_config() { + let meta = serde_json::json!({ + "openai/fileUploadConfig": { + "store_in_library": true, + } + }); + + let parsed = parse_openai_file_upload_options(meta.as_object()); + + assert_eq!( + parsed, + Some(codex_api::OpenAiFileUploadOptions { + store_in_library: true, + }) + ); +} + #[test] fn approval_required_when_read_only_false_and_destructive() { let annotations = annotations(Some(false), Some(true), /*open_world*/ None); @@ -632,6 +651,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps .expect("_codex_apps metadata should be an object"), ), openai_file_input_params: None, + openai_file_upload_options: None, }; assert_eq!( @@ -642,7 +662,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps ), Some(serde_json::json!({ crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata, - MCP_TOOL_CODEX_APPS_META_KEY: { + crate::codex_apps_mcp_tools::CODEX_APPS_META_KEY: { "resource_uri": "connector://calendar/tools/calendar_create_event", "contains_mcp_source": true, "connector_id": "calendar", @@ -810,6 +830,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata)); @@ -1374,6 +1395,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1446,6 +1468,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1521,6 +1544,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1573,6 +1597,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let mut approval_task = { @@ -1651,6 +1676,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1722,6 +1748,7 @@ async fn custom_approve_mode_blocks_when_arc_returns_interrupt_for_model() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1793,6 +1820,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( @@ -1872,6 +1900,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() { mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; for approval_mode in [ @@ -1978,6 +2007,7 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_ mcp_app_resource_uri: None, codex_apps_meta: None, openai_file_input_params: None, + openai_file_upload_options: None, }; let decision = maybe_request_mcp_tool_approval( diff --git a/codex-rs/core/src/mcp_tool_exposure.rs b/codex-rs/core/src/mcp_tool_exposure.rs index 3917a1d5e768..e67faf801733 100644 --- a/codex-rs/core/src/mcp_tool_exposure.rs +++ b/codex-rs/core/src/mcp_tool_exposure.rs @@ -7,6 +7,7 @@ use codex_mcp::ToolInfo as McpToolInfo; use codex_mcp::filter_non_codex_apps_mcp_tools_only; use codex_tools::ToolsConfig; +use crate::codex_apps_mcp_tools::is_direct_exposed_codex_apps_builtin_tool_info; use crate::config::Config; use crate::connectors; @@ -25,6 +26,9 @@ pub(crate) fn build_mcp_tool_exposure( tools_config: &ToolsConfig, ) -> McpToolExposure { let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools); + deferred_tools.extend(filter_direct_exposed_builtin_codex_apps_tools( + all_mcp_tools, + )); if let Some(connectors) = connectors { deferred_tools.extend(filter_codex_apps_mcp_tools( all_mcp_tools, @@ -52,12 +56,28 @@ pub(crate) fn build_mcp_tool_exposure( deferred_tools.remove(direct_tool_name); } + let mut direct_tools = filter_direct_exposed_builtin_codex_apps_tools(all_mcp_tools); + direct_tools.extend(filter_codex_apps_mcp_tools( + all_mcp_tools, + explicitly_enabled_connectors, + config, + )); McpToolExposure { direct_tools, deferred_tools: (!deferred_tools.is_empty()).then_some(deferred_tools), } } +fn filter_direct_exposed_builtin_codex_apps_tools( + mcp_tools: &HashMap, +) -> HashMap { + mcp_tools + .iter() + .filter(|(_, tool)| is_direct_exposed_codex_apps_builtin_tool_info(tool)) + .map(|(name, tool)| (name.clone(), tool.clone())) + .collect() +} + fn filter_codex_apps_mcp_tools( mcp_tools: &HashMap, connectors: &[connectors::AppInfo], diff --git a/codex-rs/core/src/mcp_tool_exposure_test.rs b/codex-rs/core/src/mcp_tool_exposure_test.rs index 3372291df9ac..70ab86cd9586 100644 --- a/codex-rs/core/src/mcp_tool_exposure_test.rs +++ b/codex-rs/core/src/mcp_tool_exposure_test.rs @@ -44,6 +44,22 @@ fn make_mcp_tool( tool_name: &str, connector_id: Option<&str>, connector_name: Option<&str>, +) -> ToolInfo { + make_mcp_tool_with_meta( + server_name, + tool_name, + connector_id, + connector_name, + /*meta*/ None, + ) +} + +fn make_mcp_tool_with_meta( + server_name: &str, + tool_name: &str, + connector_id: Option<&str>, + connector_name: Option<&str>, + meta: Option, ) -> ToolInfo { let tool_namespace = if server_name == CODEX_APPS_MCP_SERVER_NAME { connector_name @@ -68,7 +84,7 @@ fn make_mcp_tool( annotations: None, execution: None, icons: None, - meta: None, + meta: meta.map(rmcp::model::Meta), }, connector_id: connector_id.map(str::to_string), connector_name: connector_name.map(str::to_string), @@ -77,6 +93,18 @@ fn make_mcp_tool( } } +fn direct_exposed_builtin_codex_apps_meta() -> JsonObject { + serde_json::json!({ + "_codex_apps": { + "provider": "builtin", + "direct_expose": true, + }, + }) + .as_object() + .cloned() + .expect("tool metadata object") +} + fn numbered_mcp_tools(count: usize) -> HashMap { (0..count) .map(|index| { @@ -257,3 +285,39 @@ async fn always_defer_feature_preserves_explicit_apps() { assert!(deferred_tools.contains_key("mcp__rmcp__tool")); assert!(!deferred_tools.contains_key("mcp__codex_apps__calendar_create_event")); } + +#[tokio::test] +async fn keeps_direct_exposed_builtin_codex_apps_tools_direct_in_large_search_sets() { + let config = test_config().await; + let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await; + let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD); + mcp_tools.insert( + "mcp__codex_apps__builtin_search_file".to_string(), + make_mcp_tool_with_meta( + CODEX_APPS_MCP_SERVER_NAME, + "builtin_search_file", + /*connector_id*/ None, + /*connector_name*/ None, + Some(direct_exposed_builtin_codex_apps_meta()), + ), + ); + + let exposure = build_mcp_tool_exposure( + &mcp_tools, + /*connectors*/ None, + &[], + &config, + &tools_config, + ); + + assert_eq!( + exposure.direct_tools.into_keys().collect::>(), + vec!["mcp__codex_apps__builtin_search_file".to_string()] + ); + let deferred_tools = exposure + .deferred_tools + .as_ref() + .expect("large tool sets should be discoverable through tool_search"); + assert!(deferred_tools.contains_key("mcp__codex_apps__builtin_search_file")); + assert!(deferred_tools.contains_key("mcp__rmcp__tool_0")); +} diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index bed4a4f2fa67..b153ba80a882 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -78,7 +78,6 @@ ignore = [ # 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" }, - { id = "RUSTSEC-2026-0097", reason = "rand 0.8.5 is pulled in via age v0.11.2/codex-secrets and zbus v4.4.0/keyring; no compatible rand 0.8 fixed release, remove when transitive dependencies move to rand >=0.9.3" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library.