refactor: normalize unix module layout for exec-server and shell-escalation (#12556)

## Why
Shell execution refactoring in `exec-server` had become split between
duplicated code paths, which blocked a clean introduction of the new
reusable shell escalation flow. This commit creates a dedicated
foundation crate so later shell tooling changes can share one
implementation.

## What changed
- Added the `codex-shell-escalation` crate and moved the core escalation
pieces (`mcp` protocol/socket/session flow, policy glue) that were
previously in `exec-server` into it.
- Normalized `exec-server` Unix structure under a dedicated `unix`
module layout and kept non-Unix builds narrow.
- Wired crate/build metadata so `shell-escalation` is a first-class
workspace dependency for follow-on integration work.

## Verification
- Built and linted the stack at this commit point with `just clippy`.

[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/12556).
* #12584
* #12583
* __->__ #12556
This commit is contained in:
Michael Bolin 2026-02-23 09:28:17 -08:00 committed by GitHub
parent a606e85859
commit 5221575f23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 327 additions and 181 deletions

27
codex-rs/Cargo.lock generated
View file

@ -1786,21 +1786,19 @@ dependencies = [
"codex-execpolicy",
"codex-protocol",
"codex-shell-command",
"codex-shell-escalation",
"codex-utils-cargo-bin",
"core_test_support",
"exec_server_test_support",
"libc",
"maplit",
"path-absolutize",
"pretty_assertions",
"rmcp",
"schemars 1.2.1",
"serde",
"serde_json",
"shlex",
"socket2 0.6.2",
"tempfile",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
]
@ -2201,6 +2199,27 @@ dependencies = [
"which",
]
[[package]]
name = "codex-shell-escalation"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"codex-core",
"codex-execpolicy",
"codex-protocol",
"libc",
"path-absolutize",
"pretty_assertions",
"serde",
"serde_json",
"socket2 0.6.2",
"tempfile",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "codex-skills"
version = "0.0.0"

View file

@ -17,6 +17,7 @@ members = [
"cli",
"config",
"shell-command",
"shell-escalation",
"skills",
"core",
"hooks",
@ -113,6 +114,7 @@ codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-rmcp-client = { path = "rmcp-client" }
codex-secrets = { path = "secrets" }
codex-shell-command = { path = "shell-command" }
codex-shell-escalation = { path = "shell-escalation" }
codex-skills = { path = "skills" }
codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }

View file

@ -1,8 +1,8 @@
[package]
name = "codex-exec-server"
version.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[[bin]]
name = "codex-execve-wrapper"
@ -19,6 +19,11 @@ path = "src/lib.rs"
[lints]
workspace = true
[package.metadata.cargo-shear]
# This appears to be due to #[derive(rmcp::schemars::JsonSchema)], which
# requires use of schemars via a macro that shear cannot detect.
ignored = ["schemars"]
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
@ -27,8 +32,7 @@ codex-core = { workspace = true }
codex-execpolicy = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
libc = { workspace = true }
path-absolutize = { workspace = true }
codex-shell-escalation = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [
"auth",
"elicitation",
@ -42,25 +46,18 @@ rmcp = { workspace = true, default-features = false, features = [
"transport-streamable-http-server",
"transport-io",
] }
schemars = { version = "1.2.1" }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
shlex = { workspace = true }
socket2 = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
"process",
"rt-multi-thread",
"signal",
] }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
[dev-dependencies]
core_test_support = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
exec_server_test_support = { workspace = true }
maplit = { workspace = true }
pretty_assertions = { workspace = true }

View file

@ -1,11 +1,5 @@
#[cfg(unix)]
mod posix;
mod unix;
#[cfg(unix)]
pub use posix::main_execve_wrapper;
#[cfg(unix)]
pub use posix::main_mcp_server;
#[cfg(unix)]
pub use posix::ExecResult;
pub use unix::*;

View file

@ -67,21 +67,16 @@ use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::RuleMatch;
use codex_shell_command::is_dangerous_command::command_might_be_dangerous;
use codex_shell_escalation as shell_escalation;
use rmcp::ErrorData as McpError;
use tokio::sync::RwLock;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::{self};
use crate::posix::mcp_escalation_policy::ExecPolicyOutcome;
use crate::unix::mcp_escalation_policy::ExecPolicyOutcome;
mod escalate_client;
mod escalate_protocol;
mod escalate_server;
mod escalation_policy;
mod mcp;
mod mcp_escalation_policy;
mod socket;
mod stopwatch;
pub use mcp::ExecResult;
@ -165,7 +160,7 @@ pub async fn main_execve_wrapper() -> anyhow::Result<()> {
.init();
let ExecveWrapperCli { file, argv } = ExecveWrapperCli::parse();
let exit_code = escalate_client::run(file, argv).await?;
let exit_code = shell_escalation::run(file, argv).await?;
std::process::exit(exit_code);
}

View file

@ -1,4 +1,3 @@
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@ -10,6 +9,8 @@ use codex_core::MCP_SANDBOX_STATE_METHOD;
use codex_core::SandboxState;
use codex_execpolicy::Policy;
use codex_protocol::protocol::SandboxPolicy;
use codex_shell_escalation::EscalationPolicyFactory;
use codex_shell_escalation::run_escalate_server;
use rmcp::ErrorData as McpError;
use rmcp::RoleServer;
use rmcp::ServerHandler;
@ -19,7 +20,6 @@ use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::CustomRequest;
use rmcp::model::CustomResult;
use rmcp::model::*;
use rmcp::schemars;
use rmcp::service::RequestContext;
use rmcp::service::RunningService;
use rmcp::tool;
@ -29,11 +29,7 @@ use rmcp::transport::stdio;
use serde_json::json;
use tokio::sync::RwLock;
use crate::posix::escalate_server::EscalateServer;
use crate::posix::escalate_server::{self};
use crate::posix::escalation_policy::EscalationPolicy;
use crate::posix::mcp_escalation_policy::McpEscalationPolicy;
use crate::posix::stopwatch::Stopwatch;
use crate::unix::mcp_escalation_policy::McpEscalationPolicy;
/// Path to our patched bash.
const CODEX_BASH_PATH_ENV_VAR: &str = "CODEX_BASH_PATH";
@ -46,19 +42,7 @@ pub(crate) fn get_bash_path() -> Result<PathBuf> {
.context(format!("{CODEX_BASH_PATH_ENV_VAR} must be set"))
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ExecParams {
/// The bash string to execute.
pub command: String,
/// The working directory to execute the command in. Must be an absolute path.
pub workdir: String,
/// The timeout for the command in milliseconds.
pub timeout_ms: Option<u64>,
/// Launch Bash with -lc instead of -c: defaults to true.
pub login: Option<bool>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ExecResult {
pub exit_code: i32,
pub output: String,
@ -66,8 +50,8 @@ pub struct ExecResult {
pub timed_out: bool,
}
impl From<escalate_server::ExecResult> for ExecResult {
fn from(result: escalate_server::ExecResult) -> Self {
impl From<codex_shell_escalation::ExecResult> for ExecResult {
fn from(result: codex_shell_escalation::ExecResult) -> Self {
Self {
exit_code: result.exit_code,
output: result.output,
@ -87,10 +71,27 @@ pub struct ExecTool {
sandbox_state: Arc<RwLock<Option<SandboxState>>>,
}
trait EscalationPolicyFactory {
type Policy: EscalationPolicy + Send + Sync + 'static;
#[derive(Debug, serde::Serialize, serde::Deserialize, rmcp::schemars::JsonSchema)]
pub struct ExecParams {
/// The bash string to execute.
pub command: String,
/// The working directory to execute the command in. Must be an absolute path.
pub workdir: String,
/// The timeout for the command in milliseconds.
pub timeout_ms: Option<u64>,
/// Launch Bash with -lc instead of -c: defaults to true.
pub login: Option<bool>,
}
fn create_policy(&self, policy: Arc<RwLock<Policy>>, stopwatch: Stopwatch) -> Self::Policy;
impl From<ExecParams> for codex_shell_escalation::ExecParams {
fn from(inner: ExecParams) -> Self {
Self {
command: inner.command,
workdir: inner.workdir,
timeout_ms: inner.timeout_ms,
login: inner.login,
}
}
}
struct McpEscalationPolicyFactory {
@ -101,7 +102,11 @@ struct McpEscalationPolicyFactory {
impl EscalationPolicyFactory for McpEscalationPolicyFactory {
type Policy = McpEscalationPolicy;
fn create_policy(&self, policy: Arc<RwLock<Policy>>, stopwatch: Stopwatch) -> Self::Policy {
fn create_policy(
&self,
policy: Arc<RwLock<Policy>>,
stopwatch: codex_shell_escalation::Stopwatch,
) -> Self::Policy {
McpEscalationPolicy::new(
policy,
self.context.clone(),
@ -153,8 +158,8 @@ impl ExecTool {
use_linux_sandbox_bwrap: false,
});
let result = run_escalate_server(
params,
sandbox_state,
params.into(),
&sandbox_state,
&self.bash_path,
&self.execve_wrapper,
self.policy.clone(),
@ -172,48 +177,6 @@ impl ExecTool {
}
}
/// Runs the escalate server to execute a shell command with potential
/// escalation of execve calls.
///
/// - `exec_params` defines the shell command to run
/// - `sandbox_state` is the sandbox to use to run the shell program
/// - `shell_program` is the path to the shell program to run (e.g. /bin/bash)
/// - `execve_wrapper` is the path to the execve wrapper binary to use for
/// handling execve calls from the shell program. This is likely a symlink to
/// Codex using a special name.
/// - `policy` is the exec policy to use for deciding whether to allow or deny
/// execve calls from the shell program.
/// - `escalation_policy_factory` is a factory for creating an
/// `EscalationPolicy` to use for deciding whether to allow, deny, or prompt
/// the user for execve calls from the shell program. We use a factory here
/// because the `EscalationPolicy` may need to capture request-specific
/// context (e.g. the MCP request context) that is not available at the time
/// we create the `ExecTool`.
/// - `effective_timeout` is the timeout to use for running the shell command.
/// Implementations are encouraged to excludeany time spent prompting the
/// user.
async fn run_escalate_server(
exec_params: ExecParams,
sandbox_state: SandboxState,
shell_program: impl AsRef<Path>,
execve_wrapper: impl AsRef<Path>,
policy: Arc<RwLock<Policy>>,
escalation_policy_factory: impl EscalationPolicyFactory,
effective_timeout: Duration,
) -> anyhow::Result<crate::posix::escalate_server::ExecResult> {
let stopwatch = Stopwatch::new(effective_timeout);
let cancel_token = stopwatch.cancellation_token();
let escalate_server = EscalateServer::new(
shell_program.as_ref().to_path_buf(),
execve_wrapper.as_ref().to_path_buf(),
escalation_policy_factory.create_policy(policy, stopwatch),
);
escalate_server
.exec(exec_params, cancel_token, &sandbox_state)
.await
}
#[derive(Default)]
pub struct CodexSandboxStateUpdateMethod;
@ -307,7 +270,7 @@ mod tests {
/// `timeout_ms` fields are optional.
#[test]
fn exec_params_json_schema_matches_expected() {
let schema = schemars::schema_for!(ExecParams);
let schema = rmcp::schemars::schema_for!(ExecParams);
let actual = serde_json::to_value(schema).expect("schema should serialize");
assert_eq!(

View file

@ -2,6 +2,9 @@ use std::path::Path;
use codex_core::sandboxing::SandboxPermissions;
use codex_execpolicy::Policy;
use codex_shell_escalation::EscalateAction;
use codex_shell_escalation::EscalationPolicy;
use codex_shell_escalation::Stopwatch;
use rmcp::ErrorData as McpError;
use rmcp::RoleServer;
use rmcp::model::CreateElicitationRequestParams;
@ -9,10 +12,7 @@ use rmcp::model::CreateElicitationResult;
use rmcp::model::ElicitationAction;
use rmcp::model::ElicitationSchema;
use rmcp::service::RequestContext;
use crate::posix::escalate_protocol::EscalateAction;
use crate::posix::escalation_policy::EscalationPolicy;
use crate::posix::stopwatch::Stopwatch;
use shlex::try_join;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -59,7 +59,7 @@ impl McpEscalationPolicy {
workdir: &Path,
context: RequestContext<RoleServer>,
) -> Result<CreateElicitationResult, McpError> {
let args = shlex::try_join(argv.iter().skip(1).map(String::as_str)).unwrap_or_default();
let args = try_join(argv.iter().skip(1).map(String::as_str)).unwrap_or_default();
let command = if args.is_empty() {
file.display().to_string()
} else {
@ -104,10 +104,10 @@ impl EscalationPolicy for McpEscalationPolicy {
file: &Path,
argv: &[String],
workdir: &Path,
) -> Result<EscalateAction, rmcp::ErrorData> {
) -> anyhow::Result<EscalateAction> {
let policy = self.policy.read().await;
let outcome =
crate::posix::evaluate_exec_policy(&policy, file, argv, self.preserve_program_paths)?;
crate::unix::evaluate_exec_policy(&policy, file, argv, self.preserve_program_paths)?;
let action = match outcome {
ExecPolicyOutcome::Allow {
sandbox_permissions,

View file

@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "shell-escalation",
crate_name = "codex_shell_escalation",
)

View file

@ -0,0 +1,30 @@
[package]
name = "codex-shell-escalation"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
codex-core = { workspace = true }
codex-execpolicy = { workspace = true }
codex-protocol = { workspace = true }
libc = { workspace = true }
serde_json = { workspace = true }
path-absolutize = { workspace = true }
serde = { workspace = true, features = ["derive"] }
socket2 = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
"process",
"rt-multi-thread",
"signal",
] }
tokio-util = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }

View file

@ -0,0 +1,21 @@
#[cfg(unix)]
mod unix {
mod escalate_client;
mod escalate_protocol;
mod escalate_server;
mod escalation_policy;
mod socket;
mod stopwatch;
pub use self::escalate_client::run;
pub use self::escalate_protocol::EscalateAction;
pub use self::escalate_server::EscalationPolicyFactory;
pub use self::escalate_server::ExecParams;
pub use self::escalate_server::ExecResult;
pub use self::escalate_server::run_escalate_server;
pub use self::escalation_policy::EscalationPolicy;
pub use self::stopwatch::Stopwatch;
}
#[cfg(unix)]
pub use unix::*;

View file

@ -0,0 +1,71 @@
use async_trait::async_trait;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::escalate_protocol::EscalateAction;
use crate::escalation_policy::EscalationPolicy;
use crate::stopwatch::Stopwatch;
use crate::unix::escalate_server::EscalationPolicyFactory;
use codex_execpolicy::Policy;
#[async_trait]
pub trait ShellActionProvider: Send + Sync {
async fn determine_action(
&self,
file: &Path,
argv: &[String],
workdir: &Path,
stopwatch: &Stopwatch,
) -> anyhow::Result<EscalateAction>;
}
#[derive(Clone)]
pub struct ShellPolicyFactory {
provider: Arc<dyn ShellActionProvider>,
}
impl ShellPolicyFactory {
pub fn new<P>(provider: P) -> Self
where
P: ShellActionProvider + 'static,
{
Self {
provider: Arc::new(provider),
}
}
pub fn with_provider(provider: Arc<dyn ShellActionProvider>) -> Self {
Self { provider }
}
}
struct ShellEscalationPolicy {
provider: Arc<dyn ShellActionProvider>,
stopwatch: Stopwatch,
}
#[async_trait]
impl EscalationPolicy for ShellEscalationPolicy {
async fn determine_action(
&self,
file: &Path,
argv: &[String],
workdir: &Path,
) -> anyhow::Result<EscalateAction> {
self.provider
.determine_action(file, argv, workdir, &self.stopwatch)
.await
}
}
impl EscalationPolicyFactory for ShellPolicyFactory {
type Policy = ShellEscalationPolicy;
fn create_policy(&self, _policy: Arc<RwLock<Policy>>, stopwatch: Stopwatch) -> Self::Policy {
ShellEscalationPolicy {
provider: Arc::clone(&self.provider),
stopwatch,
}
}
}

View file

@ -5,16 +5,16 @@ use std::os::fd::OwnedFd;
use anyhow::Context as _;
use crate::posix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR;
use crate::posix::escalate_protocol::EXEC_WRAPPER_ENV_VAR;
use crate::posix::escalate_protocol::EscalateAction;
use crate::posix::escalate_protocol::EscalateRequest;
use crate::posix::escalate_protocol::EscalateResponse;
use crate::posix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
use crate::posix::escalate_protocol::SuperExecMessage;
use crate::posix::escalate_protocol::SuperExecResult;
use crate::posix::socket::AsyncDatagramSocket;
use crate::posix::socket::AsyncSocket;
use crate::unix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR;
use crate::unix::escalate_protocol::EXEC_WRAPPER_ENV_VAR;
use crate::unix::escalate_protocol::EscalateAction;
use crate::unix::escalate_protocol::EscalateRequest;
use crate::unix::escalate_protocol::EscalateResponse;
use crate::unix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
use crate::unix::escalate_protocol::SuperExecMessage;
use crate::unix::escalate_protocol::SuperExecResult;
use crate::unix::socket::AsyncDatagramSocket;
use crate::unix::socket::AsyncSocket;
fn get_escalate_client() -> anyhow::Result<AsyncDatagramSocket> {
// TODO: we should defensively require only calling this once, since AsyncSocket will take ownership of the fd.
@ -27,7 +27,7 @@ fn get_escalate_client() -> anyhow::Result<AsyncDatagramSocket> {
Ok(unsafe { AsyncDatagramSocket::from_raw_fd(client_fd) }?)
}
pub(crate) async fn run(file: String, argv: Vec<String>) -> anyhow::Result<i32> {
pub async fn run(file: String, argv: Vec<String>) -> anyhow::Result<i32> {
let handshake_client = get_escalate_client()?;
let (server, client) = AsyncSocket::pair()?;
const HANDSHAKE_MESSAGE: [u8; 1] = [0];

View file

@ -6,33 +6,33 @@ use serde::Deserialize;
use serde::Serialize;
/// 'exec-server escalate' reads this to find the inherited FD for the escalate socket.
pub(super) const ESCALATE_SOCKET_ENV_VAR: &str = "CODEX_ESCALATE_SOCKET";
pub const ESCALATE_SOCKET_ENV_VAR: &str = "CODEX_ESCALATE_SOCKET";
/// Patched shells use this to wrap exec() calls.
pub(super) const EXEC_WRAPPER_ENV_VAR: &str = "EXEC_WRAPPER";
pub const EXEC_WRAPPER_ENV_VAR: &str = "EXEC_WRAPPER";
/// Compatibility alias for older patched bash builds.
pub(super) const LEGACY_BASH_EXEC_WRAPPER_ENV_VAR: &str = "BASH_EXEC_WRAPPER";
pub const LEGACY_BASH_EXEC_WRAPPER_ENV_VAR: &str = "BASH_EXEC_WRAPPER";
/// The client sends this to the server to request an exec() call.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub(super) struct EscalateRequest {
pub struct EscalateRequest {
/// The absolute path to the executable to run, i.e. the first arg to exec.
pub(super) file: PathBuf,
pub file: PathBuf,
/// The argv, including the program name (argv[0]).
pub(super) argv: Vec<String>,
pub(super) workdir: PathBuf,
pub(super) env: HashMap<String, String>,
pub argv: Vec<String>,
pub workdir: PathBuf,
pub env: HashMap<String, String>,
}
/// The server sends this to the client to respond to an exec() request.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub(super) struct EscalateResponse {
pub(super) action: EscalateAction,
pub struct EscalateResponse {
pub action: EscalateAction,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub(super) enum EscalateAction {
pub enum EscalateAction {
/// The command should be run directly by the client.
Run,
/// The command should be escalated to the server for execution.
@ -43,12 +43,12 @@ pub(super) enum EscalateAction {
/// The client sends this to the server to forward its open FDs.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct SuperExecMessage {
pub(super) fds: Vec<RawFd>,
pub struct SuperExecMessage {
pub fds: Vec<RawFd>,
}
/// The server responds when the exec()'d command has exited.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(super) struct SuperExecResult {
pub(super) exit_code: i32,
pub struct SuperExecResult {
pub exit_code: i32,
}

View file

@ -1,35 +1,54 @@
use std::collections::HashMap;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use path_absolutize::Absolutize as _;
use codex_core::SandboxState;
use codex_core::exec::process_exec_tool_call;
use codex_core::sandboxing::SandboxPermissions;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_execpolicy::Policy;
use path_absolutize::Absolutize as _;
use tokio::process::Command;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use crate::posix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR;
use crate::posix::escalate_protocol::EXEC_WRAPPER_ENV_VAR;
use crate::posix::escalate_protocol::EscalateAction;
use crate::posix::escalate_protocol::EscalateRequest;
use crate::posix::escalate_protocol::EscalateResponse;
use crate::posix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
use crate::posix::escalate_protocol::SuperExecMessage;
use crate::posix::escalate_protocol::SuperExecResult;
use crate::posix::escalation_policy::EscalationPolicy;
use crate::posix::mcp::ExecParams;
use crate::posix::socket::AsyncDatagramSocket;
use crate::posix::socket::AsyncSocket;
use codex_core::exec::ExecExpiration;
use crate::unix::escalate_protocol::ESCALATE_SOCKET_ENV_VAR;
use crate::unix::escalate_protocol::EXEC_WRAPPER_ENV_VAR;
use crate::unix::escalate_protocol::EscalateAction;
use crate::unix::escalate_protocol::EscalateRequest;
use crate::unix::escalate_protocol::EscalateResponse;
use crate::unix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
use crate::unix::escalate_protocol::SuperExecMessage;
use crate::unix::escalate_protocol::SuperExecResult;
use crate::unix::escalation_policy::EscalationPolicy;
use crate::unix::socket::AsyncDatagramSocket;
use crate::unix::socket::AsyncSocket;
use crate::unix::stopwatch::Stopwatch;
pub(crate) struct EscalateServer {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ExecParams {
/// The bash string to execute.
pub command: String,
/// The working directory to execute the command in. Must be an absolute path.
pub workdir: String,
/// The timeout for the command in milliseconds.
pub timeout_ms: Option<u64>,
/// Launch Bash with -lc instead of -c: defaults to true.
pub login: Option<bool>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ExecResult {
pub exit_code: i32,
pub output: String,
pub duration: Duration,
pub timed_out: bool,
}
#[allow(clippy::module_name_repetitions)]
pub struct EscalateServer {
bash_path: PathBuf,
execve_wrapper: PathBuf,
policy: Arc<dyn EscalationPolicy>,
@ -78,7 +97,7 @@ impl EscalateServer {
timeout_ms: _,
login,
} = params;
let result = process_exec_tool_call(
let result = codex_core::exec::process_exec_tool_call(
codex_core::exec::ExecParams {
command: vec![
self.bash_path.to_string_lossy().to_string(),
@ -90,11 +109,11 @@ impl EscalateServer {
command,
],
cwd: PathBuf::from(&workdir),
expiration: ExecExpiration::Cancellation(cancel_rx),
expiration: codex_core::exec::ExecExpiration::Cancellation(cancel_rx),
env,
network: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,
arg0: None,
},
@ -106,16 +125,45 @@ impl EscalateServer {
)
.await?;
escalate_task.abort();
let result = ExecResult {
Ok(ExecResult {
exit_code: result.exit_code,
output: result.aggregated_output.text,
duration: result.duration,
timed_out: result.timed_out,
};
Ok(result)
})
}
}
/// Factory for creating escalation policy instances for a single shell run.
pub trait EscalationPolicyFactory {
type Policy: EscalationPolicy + Send + Sync + 'static;
fn create_policy(&self, policy: Arc<RwLock<Policy>>, stopwatch: Stopwatch) -> Self::Policy;
}
pub async fn run_escalate_server(
exec_params: ExecParams,
sandbox_state: &SandboxState,
shell_program: impl AsRef<Path>,
execve_wrapper: impl AsRef<Path>,
policy: Arc<RwLock<Policy>>,
escalation_policy_factory: impl EscalationPolicyFactory,
effective_timeout: Duration,
) -> anyhow::Result<ExecResult> {
let stopwatch = Stopwatch::new(effective_timeout);
let cancel_token = stopwatch.cancellation_token();
let escalate_server = EscalateServer::new(
shell_program.as_ref().to_path_buf(),
execve_wrapper.as_ref().to_path_buf(),
escalation_policy_factory.create_policy(policy, stopwatch),
);
escalate_server
.exec(exec_params, cancel_token, sandbox_state)
.await
}
async fn escalate_task(
socket: AsyncDatagramSocket,
policy: Arc<dyn EscalationPolicy>,
@ -136,14 +184,6 @@ async fn escalate_task(
}
}
#[derive(Debug)]
pub(crate) struct ExecResult {
pub(crate) exit_code: i32,
pub(crate) output: String,
pub(crate) duration: Duration,
pub(crate) timed_out: bool,
}
async fn handle_escalate_session_with_policy(
socket: AsyncSocket,
policy: Arc<dyn EscalationPolicy>,
@ -158,7 +198,8 @@ async fn handle_escalate_session_with_policy(
let workdir = PathBuf::from(&workdir).absolutize()?.into_owned();
let action = policy
.determine_action(file.as_path(), &argv, &workdir)
.await?;
.await
.context("failed to determine escalation action")?;
tracing::debug!("decided {action:?} for {file:?} {argv:?} {workdir:?}");
@ -253,7 +294,7 @@ mod tests {
_file: &Path,
_argv: &[String],
_workdir: &Path,
) -> Result<EscalateAction, rmcp::ErrorData> {
) -> anyhow::Result<EscalateAction> {
Ok(self.action.clone())
}
}

View file

@ -1,14 +1,14 @@
use std::path::Path;
use crate::posix::escalate_protocol::EscalateAction;
use crate::unix::escalate_protocol::EscalateAction;
/// Decides what action to take in response to an execve request from a client.
#[async_trait::async_trait]
pub(crate) trait EscalationPolicy: Send + Sync {
pub trait EscalationPolicy: Send + Sync {
async fn determine_action(
&self,
file: &Path,
argv: &[String],
workdir: &Path,
) -> Result<EscalateAction, rmcp::ErrorData>;
) -> anyhow::Result<EscalateAction>;
}

View file

@ -0,0 +1,7 @@
pub mod escalate_client;
pub mod escalate_protocol;
pub mod escalate_server;
pub mod escalation_policy;
pub mod socket;
pub mod core_shell_escalation;
pub mod stopwatch;

View file

@ -8,7 +8,7 @@ use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug)]
pub(crate) struct Stopwatch {
pub struct Stopwatch {
limit: Duration,
inner: Arc<Mutex<StopwatchState>>,
notify: Arc<Notify>,
@ -22,7 +22,7 @@ struct StopwatchState {
}
impl Stopwatch {
pub(crate) fn new(limit: Duration) -> Self {
pub fn new(limit: Duration) -> Self {
Self {
inner: Arc::new(Mutex::new(StopwatchState {
elapsed: Duration::ZERO,
@ -34,7 +34,7 @@ impl Stopwatch {
}
}
pub(crate) fn cancellation_token(&self) -> CancellationToken {
pub fn cancellation_token(&self) -> CancellationToken {
let limit = self.limit;
let token = CancellationToken::new();
let cancel = token.clone();
@ -80,7 +80,7 @@ impl Stopwatch {
/// resumes automatically when the future completes. Nested/overlapping
/// calls are reference-counted so the stopwatch only resumes when every
/// pause is lifted.
pub(crate) async fn pause_for<F, T>(&self, fut: F) -> T
pub async fn pause_for<F, T>(&self, fut: F) -> T
where
F: Future<Output = T>,
{