Codex Handbook
app-server/src/request_processors/command_exec_processor.rs 345 lines
use super::*;#[derive(Clone)]pub(crate) struct CommandExecRequestProcessor {    arg0_paths: Arg0DispatchPaths,    config: Arc<Config>,    outgoing: Arc<OutgoingMessageSender>,    config_manager: ConfigManager,    environment_manager: Arc<EnvironmentManager>,    command_exec_manager: CommandExecManager,}impl CommandExecRequestProcessor {    pub(crate) fn new(        arg0_paths: Arg0DispatchPaths,        config: Arc<Config>,        outgoing: Arc<OutgoingMessageSender>,        config_manager: ConfigManager,        environment_manager: Arc<EnvironmentManager>,    ) -> Self {        Self {            arg0_paths,            config,            outgoing,            config_manager,            environment_manager,            command_exec_manager: CommandExecManager::default(),        }    }    pub(crate) async fn one_off_command_exec(        &self,        request_id: &ConnectionRequestId,        params: CommandExecParams,    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {        self.require_local_environment()?;        self.exec_one_off_command(request_id, params)            .await            .map(|()| None)    }    pub(crate) async fn command_exec_write(        &self,        request_id: ConnectionRequestId,        params: CommandExecWriteParams,    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {        self.command_exec_manager            .write(request_id, params)            .await            .map(|response| Some(response.into()))    }    pub(crate) async fn command_exec_resize(        &self,        request_id: ConnectionRequestId,        params: CommandExecResizeParams,    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {        self.command_exec_manager            .resize(request_id, params)            .await            .map(|response| Some(response.into()))    }    pub(crate) async fn command_exec_terminate(        &self,        request_id: ConnectionRequestId,        params: CommandExecTerminateParams,    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {        self.command_exec_manager            .terminate(request_id, params)            .await            .map(|response| Some(response.into()))    }    pub(crate) async fn connection_closed(&self, connection_id: ConnectionId) {        self.command_exec_manager            .connection_closed(connection_id)            .await;    }    fn require_local_environment(&self) -> Result<(), JSONRPCErrorError> {        self.environment_manager            .try_local_environment()            .is_some()            .then_some(())            .ok_or_else(|| internal_error("local environment is not configured"))    }    async fn exec_one_off_command(        &self,        request_id: &ConnectionRequestId,        params: CommandExecParams,    ) -> Result<(), JSONRPCErrorError> {        self.exec_one_off_command_inner(request_id.clone(), params)            .await    }    async fn exec_one_off_command_inner(        &self,        request_id: ConnectionRequestId,        params: CommandExecParams,    ) -> Result<(), JSONRPCErrorError> {        tracing::debug!("ExecOneOffCommand params: {params:?}");        let request = request_id.clone();        if params.command.is_empty() {            return Err(invalid_request("command must not be empty"));        }        let CommandExecParams {            command,            process_id,            tty,            stream_stdin,            stream_stdout_stderr,            output_bytes_cap,            disable_output_cap,            disable_timeout,            timeout_ms,            cwd,            env: env_overrides,            size,            sandbox_policy,            permission_profile,        } = params;        if sandbox_policy.is_some() && permission_profile.is_some() {            return Err(invalid_request(                "`permissionProfile` cannot be combined with `sandboxPolicy`",            ));        }        if size.is_some() && !tty {            return Err(invalid_params("command/exec size requires tty: true"));        }        if disable_output_cap && output_bytes_cap.is_some() {            return Err(invalid_params(                "command/exec cannot set both outputBytesCap and disableOutputCap",            ));        }        if disable_timeout && timeout_ms.is_some() {            return Err(invalid_params(                "command/exec cannot set both timeoutMs and disableTimeout",            ));        }        let cwd = cwd.map_or_else(|| self.config.cwd.clone(), |cwd| self.config.cwd.join(cwd));        let mut env = create_env(            &self.config.permissions.shell_environment_policy,            /*thread_id*/ None,        );        if let Some(env_overrides) = env_overrides {            for (key, value) in env_overrides {                match value {                    Some(value) => {                        env.insert(key, value);                    }                    None => {                        env.remove(&key);                    }                }            }        }        let timeout_ms = match timeout_ms {            Some(timeout_ms) => match u64::try_from(timeout_ms) {                Ok(timeout_ms) => Some(timeout_ms),                Err(_) => {                    return Err(invalid_params(format!(                        "command/exec timeoutMs must be non-negative, got {timeout_ms}"                    )));                }            },            None => None,        };        let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);        let output_bytes_cap = if disable_output_cap {            None        } else {            Some(output_bytes_cap.unwrap_or(DEFAULT_OUTPUT_BYTES_CAP))        };        let expiration = if disable_timeout {            ExecExpiration::Cancellation(CancellationToken::new())        } else {            match timeout_ms {                Some(timeout_ms) => timeout_ms.into(),                None => ExecExpiration::DefaultTimeout,            }        };        let capture_policy = if disable_output_cap {            ExecCapturePolicy::FullBuffer        } else {            ExecCapturePolicy::ShellTool        };        let sandbox_cwd = if permission_profile.is_some() {            cwd.clone()        } else {            self.config.cwd.clone()        };        let (            effective_permission_profile,            network_proxy_spec,            network_proxy_permission_profile,            managed_network_requirements_enabled,            windows_sandbox_workspace_roots,        ) = if let Some(permission_profile) = permission_profile {            let overrides = ConfigOverrides {                cwd: Some(cwd.to_path_buf()),                default_permissions: Some(permission_profile),                ..Default::default()            };            let config = self                .config_manager                .load_for_cwd(                    /*request_overrides*/ None,                    overrides,                    Some(self.config.cwd.to_path_buf()),                )                .await                .map_err(|err| invalid_request(format!("invalid permission profile: {err}")))?;            if let Some(warning) = config.startup_warnings.iter().find(|warning| {                warning.contains("Configured value for `permission_profile` is disallowed")            }) {                return Err(invalid_request(format!(                    "invalid permission profile: {warning}"                )));            }            (                config.permissions.effective_permission_profile(),                config.permissions.network.clone(),                config.permissions.permission_profile().clone(),                config.managed_network_requirements_enabled(),                config.effective_workspace_roots(),            )        } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) {            self.config                .permissions                .can_set_legacy_sandbox_policy(&policy, &sandbox_cwd)                .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?;            let file_system_sandbox_policy =                codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);            let network_sandbox_policy =                codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);            let permission_profile =                codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(                    codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(&policy),                    &file_system_sandbox_policy,                    network_sandbox_policy,                );            self.config                .permissions                .can_set_permission_profile(&permission_profile)                .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?;            (                permission_profile,                self.config.permissions.network.clone(),                self.config.permissions.permission_profile().clone(),                self.config.managed_network_requirements_enabled(),                self.config.effective_workspace_roots(),            )        } else {            (                self.config.permissions.effective_permission_profile(),                self.config.permissions.network.clone(),                self.config.permissions.permission_profile().clone(),                self.config.managed_network_requirements_enabled(),                self.config.effective_workspace_roots(),            )        };        let started_network_proxy = match network_proxy_spec.as_ref() {            Some(spec) => match spec                .start_proxy(                    &network_proxy_permission_profile,                    /*policy_decider*/ None,                    /*blocked_request_observer*/ None,                    managed_network_requirements_enabled,                    NetworkProxyAuditMetadata::default(),                )                .await            {                Ok(started) => Some(started),                Err(err) => {                    return Err(internal_error(format!(                        "failed to start managed network proxy: {err}"                    )));                }            },            None => None,        };        let exec_params = ExecParams {            command,            cwd: cwd.clone(),            expiration,            capture_policy,            env,            network: started_network_proxy                .as_ref()                .map(codex_core::config::StartedNetworkProxy::proxy),            sandbox_permissions: SandboxPermissions::UseDefault,            windows_sandbox_level,            windows_sandbox_private_desktop: self                .config                .permissions                .windows_sandbox_private_desktop,            justification: None,            arg0: None,        };        let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();        let outgoing = self.outgoing.clone();        let request_for_task = request.clone();        let started_network_proxy_for_task = started_network_proxy;        let use_legacy_landlock = self.config.features.use_legacy_landlock();        let size = match size.map(crate::command_exec::terminal_size_from_protocol) {            Some(Ok(size)) => Some(size),            Some(Err(error)) => return Err(error),            None => None,        };        let exec_request = codex_core::exec::build_exec_request(            exec_params,            &effective_permission_profile,            &sandbox_cwd,            windows_sandbox_workspace_roots.as_slice(),            &codex_linux_sandbox_exe,            use_legacy_landlock,        )        .map_err(|err| internal_error(format!("exec failed: {err}")))?;        self.command_exec_manager            .start(StartCommandExecParams {                outgoing,                request_id: request_for_task,                process_id,                exec_request,                started_network_proxy: started_network_proxy_for_task,                tty,                stream_stdin,                stream_stdout_stderr,                output_bytes_cap,                size,            })            .await    }}