Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,19 @@ jobs:
brew upgrade
brew --version

# Some integration tests rely on DotSlash being installed.
# See https://github.com/openai/codex/pull/7617.
- 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 }}
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ codex-utils-readiness = { path = "utils/readiness" }
codex-utils-string = { path = "utils/string" }
codex-windows-sandbox = { path = "windows-sandbox-rs" }
core_test_support = { path = "core/tests/common" }
exec_server_test_support = { path = "exec-server/tests/common" }
mcp-types = { path = "mcp-types" }
mcp_test_support = { path = "mcp-server/tests/common" }

Expand Down
4 changes: 4 additions & 0 deletions codex-rs/exec-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,9 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }

[dev-dependencies]
assert_cmd = { workspace = true }
exec_server_test_support = { workspace = true }
maplit = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
which = { workspace = true }
3 changes: 3 additions & 0 deletions codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ pub use posix::main_execve_wrapper;

#[cfg(unix)]
pub use posix::main_mcp_server;

#[cfg(unix)]
pub use posix::ExecResult;
2 changes: 2 additions & 0 deletions codex-rs/exec-server/src/posix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ mod mcp_escalation_policy;
mod socket;
mod stopwatch;

pub use mcp::ExecResult;

/// Default value of --execve option relative to the current executable.
/// Note this must match the name of the binary as specified in Cargo.toml.
const CODEX_EXECVE_WRAPPER_EXE_NAME: &str = "codex-execve-wrapper";
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/posix/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub struct ExecParams {
pub login: Option<bool>,
}

#[derive(Debug, serde::Serialize, schemars::JsonSchema)]
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ExecResult {
pub exit_code: i32,
pub output: String,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/exec-server/tests/all.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Single integration test binary that aggregates all test modules.
// The submodules live in `tests/suite/`.
mod suite;
16 changes: 16 additions & 0 deletions codex-rs/exec-server/tests/common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "exec_server_test_support"
version.workspace = true
edition.workspace = true
license.workspace = true

[lib]
path = "lib.rs"

[dependencies]
assert_cmd = { workspace = true }
anyhow = { workspace = true }
codex-core = { workspace = true }
rmcp = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
167 changes: 167 additions & 0 deletions codex-rs/exec-server/tests/common/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use codex_core::MCP_SANDBOX_STATE_NOTIFICATION;
use codex_core::SandboxState;
use codex_core::protocol::SandboxPolicy;
use rmcp::ClientHandler;
use rmcp::ErrorData as McpError;
use rmcp::RoleClient;
use rmcp::Service;
use rmcp::model::ClientCapabilities;
use rmcp::model::ClientInfo;
use rmcp::model::CreateElicitationRequestParam;
use rmcp::model::CreateElicitationResult;
use rmcp::model::CustomClientNotification;
use rmcp::model::ElicitationAction;
use rmcp::service::RunningService;
use rmcp::transport::ConfigureCommandExt;
use rmcp::transport::TokioChildProcess;
use serde_json::json;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::Mutex;
use tokio::process::Command;

pub fn create_transport<P>(codex_home: P) -> anyhow::Result<TokioChildProcess>
where
P: AsRef<Path>,
{
let mcp_executable = assert_cmd::Command::cargo_bin("codex-exec-mcp-server")?;
let execve_wrapper = assert_cmd::Command::cargo_bin("codex-execve-wrapper")?;
let bash = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("tests")
.join("suite")
.join("bash");

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());

// Important: pipe stdio so rmcp can speak JSON-RPC over stdin/stdout
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());

// Optional but very helpful while debugging:
cmd.stderr(Stdio::inherit());
}))?;

Ok(transport)
}

pub async fn write_default_execpolicy<P>(policy: &str, codex_home: P) -> anyhow::Result<()>
where
P: AsRef<Path>,
{
let policy_dir = codex_home.as_ref().join("policy");
tokio::fs::create_dir_all(&policy_dir).await?;
tokio::fs::write(policy_dir.join("default.codexpolicy"), policy).await?;
Ok(())
}

pub async fn notify_readable_sandbox<P, S>(
sandbox_cwd: P,
codex_linux_sandbox_exe: Option<PathBuf>,
service: &RunningService<RoleClient, S>,
) -> anyhow::Result<()>
where
P: AsRef<Path>,
S: Service<RoleClient> + ClientHandler,
{
let sandbox_state = SandboxState {
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe,
sandbox_cwd: sandbox_cwd.as_ref().to_path_buf(),
};
send_sandbox_notification(sandbox_state, service).await
}

pub async fn notify_writable_sandbox_only_one_folder<P, S>(
writable_folder: P,
codex_linux_sandbox_exe: Option<PathBuf>,
service: &RunningService<RoleClient, S>,
) -> anyhow::Result<()>
where
P: AsRef<Path>,
S: Service<RoleClient> + ClientHandler,
{
let sandbox_state = SandboxState {
sandbox_policy: SandboxPolicy::WorkspaceWrite {
// Note that sandbox_cwd will already be included as a writable root
// when the sandbox policy is expanded.
writable_roots: vec![],
network_access: false,
// Disable writes to temp dir because this is a test, so
// writable_folder is likely also under /tmp and we want to be
// strict about what is writable.
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
codex_linux_sandbox_exe,
sandbox_cwd: writable_folder.as_ref().to_path_buf(),
};
send_sandbox_notification(sandbox_state, service).await
}

async fn send_sandbox_notification<S>(
sandbox_state: SandboxState,
service: &RunningService<RoleClient, S>,
) -> anyhow::Result<()>
where
S: Service<RoleClient> + ClientHandler,
{
let sandbox_state_notification = CustomClientNotification::new(
MCP_SANDBOX_STATE_NOTIFICATION,
Some(serde_json::to_value(sandbox_state)?),
);
service
.send_notification(sandbox_state_notification.into())
.await?;
Ok(())
}

pub struct InteractiveClient {
pub elicitations_to_accept: HashSet<String>,
pub elicitation_requests: Arc<Mutex<Vec<CreateElicitationRequestParam>>>,
}

impl ClientHandler for InteractiveClient {
fn get_info(&self) -> ClientInfo {
let capabilities = ClientCapabilities::builder().enable_elicitation().build();
ClientInfo {
capabilities,
..Default::default()
}
}

fn create_elicitation(
&self,
request: CreateElicitationRequestParam,
_context: rmcp::service::RequestContext<RoleClient>,
) -> impl std::future::Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_
{
self.elicitation_requests
.lock()
.unwrap()
.push(request.clone());

let accept = self.elicitations_to_accept.contains(&request.message);
async move {
if accept {
Ok(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!({ "approve": true })),
})
} else {
Ok(CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
})
}
}
}
}
Loading
Loading