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 async fn create_transport
(
codex_home: P,
dotslash_cache: P,
) -> anyhow::Result
where
P: AsRef,
{
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");
// 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());
cmd.stdout(Stdio::piped());
// Optional but very helpful while debugging:
cmd.stderr(Stdio::inherit());
}))?;
Ok(transport)
}
pub async fn write_default_execpolicy(policy: &str, codex_home: P) -> anyhow::Result<()>
where
P: AsRef,
{
let policy_dir = codex_home.as_ref().join("rules");
tokio::fs::create_dir_all(&policy_dir).await?;
tokio::fs::write(policy_dir.join("default.rules"), policy).await?;
Ok(())
}
pub async fn notify_readable_sandbox(
sandbox_cwd: P,
codex_linux_sandbox_exe: Option,
service: &RunningService,
) -> anyhow::Result<()>
where
P: AsRef,
S: Service + 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(
writable_folder: P,
codex_linux_sandbox_exe: Option,
service: &RunningService,
) -> anyhow::Result<()>
where
P: AsRef,
S: Service + 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(
sandbox_state: SandboxState,
service: &RunningService,
) -> anyhow::Result<()>
where
S: Service + 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,
pub elicitation_requests: Arc>>,
}
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,
) -> impl std::future::Future