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
12 changes: 10 additions & 2 deletions codex-rs/exec-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ name = "codex-exec-server"
version = { workspace = true }

[[bin]]
name = "codex-exec-server"
path = "src/main.rs"
name = "codex-execve-wrapper"
path = "src/bin/main_execve_wrapper.rs"

[[bin]]
name = "codex-exec-mcp-server"
path = "src/bin/main_mcp_server.rs"

[lib]
name = "codex_exec_server"
path = "src/lib.rs"

[lints]
workspace = true
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/exec-server/src/bin/main_execve_wrapper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[cfg(not(unix))]
fn main() {
eprintln!("codex-execve-wrapper is only implemented for UNIX");
std::process::exit(1);
}

#[cfg(unix)]
pub use codex_exec_server::main_execve_wrapper as main;
8 changes: 8 additions & 0 deletions codex-rs/exec-server/src/bin/main_mcp_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[cfg(not(unix))]
fn main() {
eprintln!("codex-exec-mcp-server is only implemented for UNIX");
std::process::exit(1);
}

#[cfg(unix)]
pub use codex_exec_server::main_mcp_server as main;
8 changes: 8 additions & 0 deletions codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[cfg(unix)]
mod posix;

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

#[cfg(unix)]
pub use posix::main_mcp_server;
11 changes: 0 additions & 11 deletions codex-rs/exec-server/src/main.rs

This file was deleted.

153 changes: 55 additions & 98 deletions codex-rs/exec-server/src/posix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,12 @@
//! o<-----x
//!
use std::path::Path;
use std::path::PathBuf;

use clap::Parser;
use clap::Subcommand;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::{self};

use crate::posix::escalate_protocol::EscalateAction;
use crate::posix::escalate_server::EscalateServer;
use crate::posix::escalation_policy::EscalationPolicy;
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;

mod escalate_client;
Expand All @@ -75,124 +72,84 @@ mod mcp;
mod mcp_escalation_policy;
mod socket;

/// 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";

#[derive(Parser)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
subcommand: Option<Commands>,
struct McpServerCli {
/// Executable to delegate execve(2) calls to in Bash.
#[arg(long = "execve")]
execve_wrapper: Option<PathBuf>,

/// Path to Bash that has been patched to support execve() wrapping.
#[arg(long = "bash")]
bash_path: Option<PathBuf>,
}

#[derive(Subcommand)]
enum Commands {
Escalate(EscalateArgs),
ShellExec(ShellExecArgs),
#[tokio::main]
pub async fn main_mcp_server() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.with_ansi(false)
.init();

let cli = McpServerCli::parse();
let execve_wrapper = match cli.execve_wrapper {
Some(path) => path,
None => {
let cwd = std::env::current_exe()?;
cwd.parent()
.map(|p| p.join(CODEX_EXECVE_WRAPPER_EXE_NAME))
.ok_or_else(|| {
anyhow::anyhow!("failed to determine execve wrapper path from current exe")
})?
}
};
let bash_path = match cli.bash_path {
Some(path) => path,
None => mcp::get_bash_path()?,
};

tracing::info!("Starting MCP server");
let service = mcp::serve(bash_path, execve_wrapper, dummy_exec_policy)
.await
.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;

service.waiting().await?;
Ok(())
}

/// Invoked from within the sandbox to (potentially) escalate permissions.
#[derive(Parser, Debug)]
struct EscalateArgs {
#[derive(Parser)]
pub struct ExecveWrapperCli {
file: String,

#[arg(trailing_var_arg = true)]
argv: Vec<String>,
}

impl EscalateArgs {
/// This is the escalate client. It talks to the escalate server to determine whether to exec()
/// the command directly or to proxy to the escalate server.
async fn run(self) -> anyhow::Result<i32> {
let EscalateArgs { file, argv } = self;
escalate_client::run(file, argv).await
}
}

/// Debugging command to emulate an MCP "shell" tool call.
#[derive(Parser, Debug)]
struct ShellExecArgs {
command: String,
}

#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
pub async fn main_execve_wrapper() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.with_ansi(false)
.init();

match cli.subcommand {
Some(Commands::Escalate(args)) => {
std::process::exit(args.run().await?);
}
Some(Commands::ShellExec(args)) => {
let bash_path = mcp::get_bash_path()?;
let escalate_server = EscalateServer::new(bash_path, DummyEscalationPolicy {});
let result = escalate_server
.exec(
args.command.clone(),
std::env::vars().collect(),
std::env::current_dir()?,
None,
)
.await?;
println!("{result:?}");
std::process::exit(result.exit_code);
}
None => {
let bash_path = mcp::get_bash_path()?;

tracing::info!("Starting MCP server");
let service = mcp::serve(bash_path, dummy_exec_policy)
.await
.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;

service.waiting().await?;
Ok(())
}
}
let ExecveWrapperCli { file, argv } = ExecveWrapperCli::parse();
let exit_code = escalate_client::run(file, argv).await?;
std::process::exit(exit_code);
}

// TODO: replace with execpolicy2

struct DummyEscalationPolicy;

#[async_trait::async_trait]
impl EscalationPolicy for DummyEscalationPolicy {
async fn determine_action(
&self,
file: &Path,
argv: &[String],
workdir: &Path,
) -> Result<EscalateAction, rmcp::ErrorData> {
let outcome = dummy_exec_policy(file, argv, workdir);
let action = match outcome {
ExecPolicyOutcome::Allow {
run_with_escalated_permissions,
} => {
if run_with_escalated_permissions {
EscalateAction::Escalate
} else {
EscalateAction::Run
}
}
ExecPolicyOutcome::Forbidden => EscalateAction::Deny {
reason: Some("Execution forbidden by policy".to_string()),
},
ExecPolicyOutcome::Prompt { .. } => EscalateAction::Deny {
reason: Some("Could not prompt user for permission".to_string()),
},
};
Ok(action)
}
}

fn dummy_exec_policy(file: &Path, argv: &[String], _workdir: &Path) -> ExecPolicyOutcome {
if file.ends_with("/rm") {
if file.ends_with("rm") {
ExecPolicyOutcome::Forbidden
} else if file.ends_with("/git") {
} else if file.ends_with("git") {
ExecPolicyOutcome::Prompt {
run_with_escalated_permissions: false,
}
Expand Down
18 changes: 13 additions & 5 deletions codex-rs/exec-server/src/posix/escalate_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ use crate::posix::socket::AsyncSocket;

pub(crate) struct EscalateServer {
bash_path: PathBuf,
execve_wrapper: PathBuf,
policy: Arc<dyn EscalationPolicy>,
}

impl EscalateServer {
pub fn new<P>(bash_path: PathBuf, policy: P) -> Self
pub fn new<P>(bash_path: PathBuf, execve_wrapper: PathBuf, policy: P) -> Self
where
P: EscalationPolicy + Send + Sync + 'static,
{
Self {
bash_path,
execve_wrapper,
policy: Arc::new(policy),
}
}
Expand All @@ -60,8 +62,15 @@ impl EscalateServer {
);
env.insert(
BASH_EXEC_WRAPPER_ENV_VAR.to_string(),
format!("{} escalate", std::env::current_exe()?.to_string_lossy()),
self.execve_wrapper.to_string_lossy().to_string(),
);

// TODO: use the sandbox policy and cwd from the calling client.
// Note that sandbox_cwd is ignored for ReadOnly, but needs to be legit
// for `SandboxPolicy::WorkspaceWrite`.
let sandbox_policy = SandboxPolicy::ReadOnly;
let sandbox_cwd = PathBuf::from("/__NONEXISTENT__");

let result = process_exec_tool_call(
codex_core::exec::ExecParams {
command: vec![
Expand All @@ -77,9 +86,8 @@ impl EscalateServer {
arg0: None,
},
get_platform_sandbox().unwrap_or(SandboxType::None),
// TODO: use the sandbox policy and cwd from the calling client
&SandboxPolicy::ReadOnly,
&PathBuf::from("/__NONEXISTENT__"), // This is ignored for ReadOnly
&sandbox_policy,
&sandbox_cwd,
&None,
None,
)
Expand Down
8 changes: 6 additions & 2 deletions codex-rs/exec-server/src/posix/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,17 @@ impl From<escalate_server::ExecResult> for ExecResult {
pub struct ExecTool {
tool_router: ToolRouter<ExecTool>,
bash_path: PathBuf,
execve_wrapper: PathBuf,
policy: ExecPolicy,
}

#[tool_router]
impl ExecTool {
pub fn new(bash_path: PathBuf, policy: ExecPolicy) -> Self {
pub fn new(bash_path: PathBuf, execve_wrapper: PathBuf, policy: ExecPolicy) -> Self {
Self {
tool_router: Self::tool_router(),
bash_path,
execve_wrapper,
policy,
}
}
Expand All @@ -87,6 +89,7 @@ impl ExecTool {
) -> Result<CallToolResult, McpError> {
let escalate_server = EscalateServer::new(
self.bash_path.clone(),
self.execve_wrapper.clone(),
McpEscalationPolicy::new(self.policy, context),
);
let result = escalate_server
Expand Down Expand Up @@ -130,8 +133,9 @@ impl ServerHandler for ExecTool {

pub(crate) async fn serve(
bash_path: PathBuf,
execve_wrapper: PathBuf,
policy: ExecPolicy,
) -> Result<RunningService<RoleServer, ExecTool>, rmcp::service::ServerInitializeError> {
let tool = ExecTool::new(bash_path, policy);
let tool = ExecTool::new(bash_path, execve_wrapper, policy);
tool.serve(stdio()).await
}
Loading