Codex Handbook
core/src/tools/runtimes/unified_exec.rs 605 lines
/*Runtime: unified execHandles approval + sandbox orchestration for unified exec requests, delegating tothe process manager to spawn PTYs once an ExecRequest is prepared.*/use crate::command_canonicalization::canonicalize_command_for_approval;use crate::exec::ExecCapturePolicy;use crate::exec::ExecExpiration;use crate::guardian::GuardianApprovalRequest;use crate::guardian::GuardianNetworkAccessTrigger;use crate::guardian::review_approval_request;use crate::sandboxing::ExecOptions;use crate::sandboxing::ExecServerEnvConfig;use crate::sandboxing::SandboxPermissions;use crate::session::turn_context::TurnEnvironment;use crate::shell::ShellType;use crate::tools::flat_tool_name;use crate::tools::network_approval::NetworkApprovalMode;use crate::tools::network_approval::NetworkApprovalSpec;use crate::tools::runtimes::RuntimePathPrepends;#[cfg(unix)]use crate::tools::runtimes::apply_zsh_fork_path_prepend;use crate::tools::runtimes::build_sandbox_command;use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox;use crate::tools::runtimes::exec_env_for_sandbox_permissions;use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;use crate::tools::runtimes::shell::zsh_fork_backend;use crate::tools::sandboxing::Approvable;use crate::tools::sandboxing::ApprovalCtx;use crate::tools::sandboxing::ExecApprovalRequirement;use crate::tools::sandboxing::PermissionRequestPayload;use crate::tools::sandboxing::SandboxAttempt;use crate::tools::sandboxing::Sandboxable;use crate::tools::sandboxing::ToolCtx;use crate::tools::sandboxing::ToolError;use crate::tools::sandboxing::ToolRuntime;use crate::tools::sandboxing::managed_network_for_sandbox_permissions;use crate::tools::sandboxing::sandbox_permissions_preserving_denied_reads;use crate::tools::sandboxing::with_cached_approval;use crate::unified_exec::NoopSpawnLifecycle;use crate::unified_exec::UnifiedExecError;use crate::unified_exec::UnifiedExecProcess;use crate::unified_exec::UnifiedExecProcessManager;use codex_network_proxy::NetworkProxy;use codex_protocol::error::CodexErr;use codex_protocol::error::SandboxErr;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::protocol::ReviewDecision;use codex_sandboxing::SandboxablePreference;use codex_shell_command::powershell::prefix_powershell_script_with_utf8;use codex_tools::UnifiedExecShellMode;use codex_utils_absolute_path::AbsolutePathBuf;use futures::future::BoxFuture;use std::collections::HashMap;use tokio_util::sync::CancellationToken;/// Request payload used by the unified-exec runtime after approvals and/// sandbox preferences have been resolved for the current turn.#[derive(Clone, Debug)]pub struct UnifiedExecRequest {    pub command: Vec<String>,    pub shell_type: ShellType,    pub hook_command: String,    pub process_id: i32,    pub cwd: AbsolutePathBuf,    pub sandbox_cwd: AbsolutePathBuf,    pub turn_environment: TurnEnvironment,    pub env: HashMap<String, String>,    pub exec_server_env_config: Option<ExecServerEnvConfig>,    pub explicit_env_overrides: HashMap<String, String>,    pub network: Option<NetworkProxy>,    pub tty: bool,    pub sandbox_permissions: SandboxPermissions,    pub additional_permissions: Option<AdditionalPermissionProfile>,    #[cfg(unix)]    pub additional_permissions_preapproved: bool,    pub justification: Option<String>,    pub exec_approval_requirement: ExecApprovalRequirement,}/// Cache key for approval decisions that can be reused across equivalent/// unified-exec launches.#[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)]pub struct UnifiedExecApprovalKey {    pub command: Vec<String>,    pub cwd: AbsolutePathBuf,    pub tty: bool,    pub sandbox_permissions: SandboxPermissions,    pub additional_permissions: Option<AdditionalPermissionProfile>,}/// Runtime adapter that keeps policy and sandbox orchestration on the/// unified-exec side while delegating process startup to the manager.pub struct UnifiedExecRuntime<'a> {    manager: &'a UnifiedExecProcessManager,    shell_mode: UnifiedExecShellMode,}fn unified_exec_options(    network_denial_cancellation_token: Option<CancellationToken>,) -> ExecOptions {    let mut expiration = ExecExpiration::DefaultTimeout;    if let Some(cancellation) = network_denial_cancellation_token {        expiration = expiration.with_cancellation(cancellation);    }    ExecOptions {        expiration,        capture_policy: ExecCapturePolicy::ShellTool,    }}impl<'a> UnifiedExecRuntime<'a> {    /// Creates a runtime bound to the shared unified-exec process manager.    pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self {        Self {            manager,            shell_mode,        }    }}impl Sandboxable for UnifiedExecRuntime<'_> {    fn sandbox_preference(&self) -> SandboxablePreference {        SandboxablePreference::Auto    }    fn escalate_on_failure(&self) -> bool {        true    }}impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {    type ApprovalKey = UnifiedExecApprovalKey;    fn approval_keys(&self, req: &UnifiedExecRequest) -> Vec<Self::ApprovalKey> {        vec![UnifiedExecApprovalKey {            command: canonicalize_command_for_approval(&req.command),            cwd: req.cwd.clone(),            tty: req.tty,            sandbox_permissions: req.sandbox_permissions,            additional_permissions: req.additional_permissions.clone(),        }]    }    fn start_approval_async<'b>(        &'b mut self,        req: &'b UnifiedExecRequest,        ctx: ApprovalCtx<'b>,    ) -> BoxFuture<'b, ReviewDecision> {        let keys = self.approval_keys(req);        let session = ctx.session;        let turn = ctx.turn;        let call_id = ctx.call_id.to_string();        let command = req.command.clone();        let cwd = req.cwd.clone();        let retry_reason = ctx.retry_reason.clone();        let reason = retry_reason.clone().or_else(|| req.justification.clone());        let guardian_review_id = ctx.guardian_review_id.clone();        Box::pin(async move {            if let Some(review_id) = guardian_review_id {                return review_approval_request(                    session,                    turn,                    review_id,                    GuardianApprovalRequest::ExecCommand {                        id: call_id,                        command,                        cwd: cwd.clone(),                        sandbox_permissions: req.sandbox_permissions,                        additional_permissions: req.additional_permissions.clone(),                        justification: req.justification.clone(),                        tty: req.tty,                    },                    retry_reason,                )                .await;            }            with_cached_approval(&session.services, "unified_exec", keys, || async move {                let available_decisions = None;                session                    .request_command_approval(                        turn,                        call_id,                        /*approval_id*/ None,                        command,                        cwd.clone(),                        reason,                        ctx.network_approval_context.clone(),                        req.exec_approval_requirement                            .proposed_execpolicy_amendment()                            .cloned(),                        req.additional_permissions.clone(),                        available_decisions,                    )                    .await            })            .await        })    }    fn exec_approval_requirement(        &self,        req: &UnifiedExecRequest,    ) -> Option<ExecApprovalRequirement> {        Some(req.exec_approval_requirement.clone())    }    fn permission_request_payload(        &self,        req: &UnifiedExecRequest,    ) -> Option<PermissionRequestPayload> {        Some(PermissionRequestPayload::bash(            req.hook_command.clone(),            req.justification.clone(),        ))    }    fn sandbox_permissions(&self, req: &UnifiedExecRequest) -> SandboxPermissions {        req.sandbox_permissions    }}impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRuntime<'a> {    fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b AbsolutePathBuf> {        Some(&req.sandbox_cwd)    }    fn network_approval_spec(        &self,        req: &UnifiedExecRequest,        ctx: &ToolCtx,    ) -> Option<NetworkApprovalSpec> {        let file_system_sandbox_policy = ctx.turn.file_system_sandbox_policy();        let sandbox_permissions = sandbox_permissions_preserving_denied_reads(            req.sandbox_permissions,            &file_system_sandbox_policy,        );        let network =            managed_network_for_sandbox_permissions(req.network.as_ref(), sandbox_permissions)?;        Some(NetworkApprovalSpec {            network: Some(network.clone()),            mode: NetworkApprovalMode::Deferred,            trigger: GuardianNetworkAccessTrigger {                call_id: ctx.call_id.clone(),                tool_name: flat_tool_name(&ctx.tool_name).into_owned(),                command: req.command.clone(),                cwd: req.cwd.clone(),                sandbox_permissions: req.sandbox_permissions,                additional_permissions: req.additional_permissions.clone(),                justification: req.justification.clone(),                tty: Some(req.tty),            },            command: req.hook_command.clone(),        })    }    async fn run(        &mut self,        req: &UnifiedExecRequest,        attempt: &SandboxAttempt<'_>,        ctx: &ToolCtx,    ) -> Result<UnifiedExecProcess, ToolError> {        let base_command = &req.command;        let session_shell = ctx.session.user_shell();        let shell = req            .turn_environment            .shell            .as_ref()            .unwrap_or(session_shell.as_ref());        let shell_snapshot_location = req.turn_environment.shell_snapshot(&req.cwd);        let (file_system_sandbox_policy, _) = attempt.permissions.to_runtime_permissions();        let launch_sandbox_permissions = sandbox_permissions_preserving_denied_reads(            req.sandbox_permissions,            &file_system_sandbox_policy,        );        let managed_network = managed_network_for_sandbox_permissions(            req.network.as_ref(),            launch_sandbox_permissions,        );        let mut env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);        if let Some(network) = managed_network {            network.apply_to_env(&mut env);        }        let environment_is_remote = req.turn_environment.environment.is_remote();        let explicit_env_overrides = req.explicit_env_overrides.clone();        #[cfg(unix)]        let runtime_path_prepends = {            let mut runtime_path_prepends = RuntimePathPrepends::default();            if !environment_is_remote {                crate::tools::runtimes::apply_package_path_prepend(                    &mut env,                    &mut runtime_path_prepends,                );            }            if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {                apply_zsh_fork_path_prepend(                    &mut env,                    &mut runtime_path_prepends,                    zsh_fork_config.shell_zsh_path.as_path(),                );            }            runtime_path_prepends        };        #[cfg(not(unix))]        let runtime_path_prepends = RuntimePathPrepends::default();        let command = if environment_is_remote {            base_command.to_vec()        } else {            maybe_wrap_shell_lc_with_snapshot(                base_command,                shell,                shell_snapshot_location.as_ref(),                &explicit_env_overrides,                &env,                &runtime_path_prepends,            )        };        let command = disable_powershell_profile_for_elevated_windows_sandbox(            &command,            Some(&req.shell_type),            attempt.sandbox,            attempt.windows_sandbox_level,        );        let command = if matches!(req.shell_type, ShellType::PowerShell) {            prefix_powershell_script_with_utf8(&command)        } else {            command        };        if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {            let command =                build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())                    .map_err(|error| match error {                        ToolError::Rejected(_) => {                            ToolError::Rejected("missing command line for PTY".to_string())                        }                        error @ ToolError::Codex(_) => error,                    })?;            let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());            let mut exec_env = attempt                .env_for(command, options, managed_network)                .map_err(ToolError::Codex)?;            exec_env.exec_server_env_config = req.exec_server_env_config.clone();            match zsh_fork_backend::maybe_prepare_unified_exec(                req,                attempt,                ctx,                exec_env,                zsh_fork_config,            )            .await?            {                Some(prepared) => {                    if req.turn_environment.environment.is_remote() {                        return Err(ToolError::Rejected(                            "unified_exec zsh-fork is not supported for remote environments"                                .to_string(),                        ));                    }                    return self                        .manager                        .open_session_with_exec_env(                            req.process_id,                            &prepared.exec_request,                            req.tty,                            prepared.spawn_lifecycle,                            req.turn_environment.environment.as_ref(),                        )                        .await                        .map_err(|err| match err {                            UnifiedExecError::SandboxDenied { output, .. } => {                                ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {                                    output: Box::new(output),                                    network_policy_decision: None,                                }))                            }                            other => ToolError::Rejected(other.to_string()),                        });                }                None => {                    tracing::warn!(                        "UnifiedExec ZshFork backend specified, but conditions for using it were not met, falling back to direct execution",                    );                }            }        }        let command =            build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())                .map_err(|error| match error {                    ToolError::Rejected(_) => {                        ToolError::Rejected("missing command line for PTY".to_string())                    }                    error @ ToolError::Codex(_) => error,                })?;        let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());        let mut exec_env = attempt            .env_for(command, options, managed_network)            .map_err(ToolError::Codex)?;        exec_env.exec_server_env_config = req.exec_server_env_config.clone();        self.manager            .open_session_with_exec_env(                req.process_id,                &exec_env,                req.tty,                Box::new(NoopSpawnLifecycle),                req.turn_environment.environment.as_ref(),            )            .await            .map_err(|err| match err {                UnifiedExecError::SandboxDenied { output, .. } => {                    ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {                        output: Box::new(output),                        network_policy_decision: None,                    }))                }                other => ToolError::Rejected(other.to_string()),            })    }}#[cfg(test)]mod tests {    use super::*;    use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS;    use crate::tools::sandboxing::ToolRuntime;    use codex_exec_server::Environment;    use codex_exec_server::LOCAL_ENVIRONMENT_ID;    use codex_tools::ZshForkConfig;    use codex_utils_path_uri::PathUri;    use std::sync::Arc;    use std::time::Duration;    use tempfile::tempdir;    fn test_turn_environment(cwd: AbsolutePathBuf) -> TurnEnvironment {        TurnEnvironment::new(            LOCAL_ENVIRONMENT_ID.to_string(),            Arc::new(Environment::default_for_tests()),            PathUri::from_abs_path(&cwd),            /*shell*/ None,        )    }    #[test]    fn unified_exec_options_combines_default_timeout_with_network_denial_cancellation() {        let cancellation = CancellationToken::new();        let options = unified_exec_options(Some(cancellation.clone()));        assert_eq!(options.capture_policy, ExecCapturePolicy::ShellTool);        match options.expiration {            ExecExpiration::TimeoutOrCancellation {                timeout,                cancellation: actual,            } => {                assert_eq!(                    timeout,                    Duration::from_millis(DEFAULT_EXEC_COMMAND_TIMEOUT_MS)                );                cancellation.cancel();                assert!(actual.is_cancelled());            }            other => panic!("expected timeout-or-cancellation expiration, got {other:?}"),        }    }    #[tokio::test]    async fn unified_exec_uses_the_trusted_sandbox_cwd() {        let cwd_dir = tempdir().expect("create process temp dir");        let sandbox_dir = tempdir().expect("create sandbox temp dir");        let cwd =            AbsolutePathBuf::try_from(cwd_dir.path().to_path_buf()).expect("absolute temp dir");        let sandbox_cwd = AbsolutePathBuf::try_from(sandbox_dir.path().to_path_buf())            .expect("absolute sandbox temp dir");        let manager = UnifiedExecProcessManager::default();        let runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct);        let request = UnifiedExecRequest {            command: vec!["pwd".to_string()],            shell_type: ShellType::Sh,            hook_command: "pwd".to_string(),            process_id: 1000,            cwd,            sandbox_cwd: sandbox_cwd.clone(),            turn_environment: test_turn_environment(sandbox_cwd.clone()),            env: HashMap::new(),            exec_server_env_config: None,            explicit_env_overrides: HashMap::new(),            network: None,            tty: false,            sandbox_permissions: SandboxPermissions::UseDefault,            additional_permissions: None,            #[cfg(unix)]            additional_permissions_preapproved: false,            justification: None,            exec_approval_requirement: ExecApprovalRequirement::Skip {                bypass_sandbox: false,                proposed_execpolicy_amendment: None,            },        };        assert_eq!(runtime.sandbox_cwd(&request), Some(&sandbox_cwd));    }    #[tokio::test]    async fn zsh_fork_first_attempt_preserves_parent_sandbox_override() {        let manager = UnifiedExecProcessManager::default();        let request = test_request(            SandboxPermissions::RequireEscalated,            ExecApprovalRequirement::NeedsApproval {                reason: None,                proposed_execpolicy_amendment: None,            },        );        let direct_runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct);        let zsh_fork_runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());        assert_eq!(            direct_runtime.sandbox_permissions(&request),            SandboxPermissions::RequireEscalated,            "direct unified exec should preserve a parent require_escalated request"        );        assert_eq!(            zsh_fork_runtime.sandbox_permissions(&request),            SandboxPermissions::RequireEscalated,            "zsh-fork unified exec should preserve the same parent require_escalated request"        );    }    #[tokio::test]    async fn zsh_fork_first_attempt_preserves_additional_permissions_request() {        let manager = UnifiedExecProcessManager::default();        let request = test_request(            SandboxPermissions::WithAdditionalPermissions,            ExecApprovalRequirement::NeedsApproval {                reason: None,                proposed_execpolicy_amendment: None,            },        );        let zsh_fork_runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());        assert_eq!(            zsh_fork_runtime.sandbox_permissions(&request),            SandboxPermissions::WithAdditionalPermissions,            "zsh-fork unified exec should keep bounded additional-permissions requests sandboxed"        );    }    #[tokio::test]    async fn zsh_fork_execpolicy_allow_preserves_parent_sandbox_override() {        let manager = UnifiedExecProcessManager::default();        let request = test_request(            SandboxPermissions::UseDefault,            ExecApprovalRequirement::Skip {                bypass_sandbox: true,                proposed_execpolicy_amendment: None,            },        );        let runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());        assert_eq!(            runtime.exec_approval_requirement(&request),            Some(ExecApprovalRequirement::Skip {                bypass_sandbox: true,                proposed_execpolicy_amendment: None,            }),            "zsh-fork unified exec should preserve exec-policy allow decisions that bypass the sandbox"        );    }    fn test_request(        sandbox_permissions: SandboxPermissions,        exec_approval_requirement: ExecApprovalRequirement,    ) -> UnifiedExecRequest {        let cwd = AbsolutePathBuf::try_from(std::env::current_dir().unwrap())            .expect("current dir is absolute");        UnifiedExecRequest {            command: vec!["zsh".to_string(), "-c".to_string(), "echo hi".to_string()],            shell_type: ShellType::Zsh,            hook_command: "echo hi".to_string(),            process_id: 1000,            cwd: cwd.clone(),            sandbox_cwd: cwd.clone(),            turn_environment: test_turn_environment(cwd),            env: HashMap::new(),            exec_server_env_config: None,            explicit_env_overrides: HashMap::new(),            network: None,            tty: false,            sandbox_permissions,            additional_permissions: None,            #[cfg(unix)]            additional_permissions_preapproved: false,            justification: None,            exec_approval_requirement,        }    }    fn zsh_fork_mode() -> UnifiedExecShellMode {        let cwd = std::env::current_dir().expect("read current dir");        UnifiedExecShellMode::ZshFork(ZshForkConfig {            shell_zsh_path: AbsolutePathBuf::try_from(cwd.join("zsh")).expect("absolute zsh path"),            main_execve_wrapper_exe: AbsolutePathBuf::try_from(cwd.join("execve-wrapper"))                .expect("absolute wrapper path"),        })    }}