diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 0be45540c1c..13fcaacbbdb 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -412,14 +412,6 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@v2 - - name: Pre-fetch DotSlash artifacts - # The Bash wrapper is not available on Windows. - if: ${{ !startsWith(matrix.runner, 'windows') }} - shell: bash - run: | - set -euo pipefail - dotslash -- fetch exec-server/tests/suite/bash - - uses: dtolnay/rust-toolchain@1.90 with: targets: ${{ matrix.target }} diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index c6df5c32c7c..0c295282209 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -23,7 +23,10 @@ use std::sync::Arc; use std::sync::Mutex; use tokio::process::Command; -pub fn create_transport

(codex_home: P) -> anyhow::Result +pub async fn create_transport

( + codex_home: P, + dotslash_cache: P, +) -> anyhow::Result where P: AsRef, { @@ -36,11 +39,23 @@ where .join("suite") .join("bash"); + // Need to ensure the artifact associated with the bash DotSlash file is + // available before it is run in a read-only sandbox. + let status = Command::new("dotslash") + .arg("--") + .arg("fetch") + .arg(bash.clone()) + .env("DOTSLASH_CACHE", dotslash_cache.as_ref()) + .status() + .await?; + assert!(status.success(), "dotslash fetch failed: {status:?}"); + let transport = TokioChildProcess::new(Command::new(mcp_executable.get_program()).configure(|cmd| { cmd.arg("--bash").arg(bash); cmd.arg("--execve").arg(execve_wrapper.get_program()); cmd.env("CODEX_HOME", codex_home.as_ref()); + cmd.env("DOTSLASH_CACHE", dotslash_cache.as_ref()); // Important: pipe stdio so rmcp can speak JSON-RPC over stdin/stdout cmd.stdin(Stdio::piped()); diff --git a/codex-rs/exec-server/tests/suite/accept_elicitation.rs b/codex-rs/exec-server/tests/suite/accept_elicitation.rs index 2093f9a5777..1858deeecba 100644 --- a/codex-rs/exec-server/tests/suite/accept_elicitation.rs +++ b/codex-rs/exec-server/tests/suite/accept_elicitation.rs @@ -1,9 +1,12 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use std::borrow::Cow; +use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; +use anyhow::Context; use anyhow::Result; +use anyhow::ensure; use codex_exec_server::ExecResult; use exec_server_test_support::InteractiveClient; use exec_server_test_support::create_transport; @@ -17,6 +20,7 @@ use rmcp::model::CallToolResult; use rmcp::model::CreateElicitationRequestParam; use rmcp::model::object; use serde_json::json; +use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::symlink; use tempfile::TempDir; @@ -42,7 +46,9 @@ prefix_rule( codex_home.as_ref(), ) .await?; - let transport = create_transport(codex_home.as_ref())?; + let dotslash_cache_temp_dir = TempDir::new()?; + let dotslash_cache = dotslash_cache_temp_dir.path(); + let transport = create_transport(codex_home.as_ref(), dotslash_cache).await?; // Create an MCP client that approves expected elicitation messages. let project_root = TempDir::new()?; @@ -68,11 +74,8 @@ prefix_rule( let linux_sandbox_exe_folder = TempDir::new()?; let codex_linux_sandbox_exe = if cfg!(target_os = "linux") { let codex_linux_sandbox_exe = linux_sandbox_exe_folder.path().join("codex-linux-sandbox"); - let codex_cli = assert_cmd::Command::cargo_bin("codex")? - .get_program() - .to_os_string(); - let codex_cli_path = std::path::PathBuf::from(codex_cli); - symlink(&codex_cli_path, &codex_linux_sandbox_exe)?; + let codex_cli = ensure_codex_cli()?; + symlink(&codex_cli, &codex_linux_sandbox_exe)?; Some(codex_linux_sandbox_exe) } else { None @@ -129,3 +132,32 @@ prefix_rule( Ok(()) } + +fn ensure_codex_cli() -> Result { + let codex_cli = PathBuf::from( + assert_cmd::Command::cargo_bin("codex")? + .get_program() + .to_os_string(), + ); + + let metadata = codex_cli.metadata().with_context(|| { + format!( + "failed to read metadata for codex binary at {}", + codex_cli.display() + ) + })?; + ensure!( + metadata.is_file(), + "expected codex binary at {} to be a file; run `cargo build -p codex-cli --bin codex` before this test", + codex_cli.display() + ); + + let mode = metadata.permissions().mode(); + ensure!( + mode & 0o111 != 0, + "codex binary at {} is not executable (mode {mode:o}); run `cargo build -p codex-cli --bin codex` before this test", + codex_cli.display() + ); + + Ok(codex_cli) +} diff --git a/codex-rs/exec-server/tests/suite/list_tools.rs b/codex-rs/exec-server/tests/suite/list_tools.rs index 17505c7613c..2f3d412df7d 100644 --- a/codex-rs/exec-server/tests/suite/list_tools.rs +++ b/codex-rs/exec-server/tests/suite/list_tools.rs @@ -22,7 +22,9 @@ async fn list_tools() -> Result<()> { policy_dir.join("default.codexpolicy"), r#"prefix_rule(pattern=["ls"], decision="prompt")"#, )?; - let transport = create_transport(codex_home.path())?; + let dotslash_cache_temp_dir = TempDir::new()?; + let dotslash_cache = dotslash_cache_temp_dir.path(); + let transport = create_transport(codex_home.path(), dotslash_cache).await?; let service = ().serve(transport).await?; let tools = service.list_tools(Default::default()).await?.tools;