/* Runtime: shell Executes shell requests under the orchestrator: asks for approval when needed, builds a CommandSpec, and runs it under the current SandboxAttempt. */ use crate::exec::ExecToolCallOutput; use crate::sandboxing::execute_env; use crate::tools::runtimes::build_command_spec; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalCtx; use crate::tools::sandboxing::ApprovalRequirement; use crate::tools::sandboxing::ProvidesSandboxRetryData; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::SandboxRetryData; use crate::tools::sandboxing::Sandboxable; use crate::tools::sandboxing::SandboxablePreference; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::with_cached_approval; use codex_protocol::protocol::ReviewDecision; use futures::future::BoxFuture; use std::path::PathBuf; #[derive(Clone, Debug)] pub struct ShellRequest { pub command: Vec, pub cwd: PathBuf, pub timeout_ms: Option, pub env: std::collections::HashMap, pub with_escalated_permissions: Option, pub justification: Option, pub approval_requirement: ApprovalRequirement, pub approval_allow_prefix: Option>, } impl ProvidesSandboxRetryData for ShellRequest { fn sandbox_retry_data(&self) -> Option { Some(SandboxRetryData { command: self.command.clone(), cwd: self.cwd.clone(), }) } } #[derive(Default)] pub struct ShellRuntime; #[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct ApprovalKey { command: Vec, cwd: PathBuf, escalated: bool, } impl ShellRuntime { pub fn new() -> Self { Self } fn stdout_stream(ctx: &ToolCtx<'_>) -> Option { Some(crate::exec::StdoutStream { sub_id: ctx.turn.sub_id.clone(), call_id: ctx.call_id.clone(), tx_event: ctx.session.get_tx_event(), }) } } impl Sandboxable for ShellRuntime { fn sandbox_preference(&self) -> SandboxablePreference { SandboxablePreference::Auto } fn escalate_on_failure(&self) -> bool { true } } impl Approvable for ShellRuntime { type ApprovalKey = ApprovalKey; fn approval_key(&self, req: &ShellRequest) -> Self::ApprovalKey { ApprovalKey { command: req.command.clone(), cwd: req.cwd.clone(), escalated: req.with_escalated_permissions.unwrap_or(false), } } fn start_approval_async<'a>( &'a mut self, req: &'a ShellRequest, ctx: ApprovalCtx<'a>, ) -> BoxFuture<'a, ReviewDecision> { let key = self.approval_key(req); let command = req.command.clone(); let cwd = req.cwd.clone(); let reason = ctx .retry_reason .clone() .or_else(|| req.justification.clone()); let risk = ctx.risk.clone(); let allow_prefix = req.approval_allow_prefix.clone(); let session = ctx.session; let turn = ctx.turn; let call_id = ctx.call_id.to_string(); Box::pin(async move { with_cached_approval(&session.services, key, move || async move { session .request_command_approval( turn, call_id, command, cwd, reason, risk, allow_prefix, ) .await }) .await }) } fn approval_requirement(&self, req: &ShellRequest) -> Option { Some(req.approval_requirement.clone()) } fn wants_escalated_first_attempt(&self, req: &ShellRequest) -> bool { req.with_escalated_permissions.unwrap_or(false) } } impl ToolRuntime for ShellRuntime { async fn run( &mut self, req: &ShellRequest, attempt: &SandboxAttempt<'_>, ctx: &ToolCtx<'_>, ) -> Result { let spec = build_command_spec( &req.command, &req.cwd, &req.env, req.timeout_ms, req.with_escalated_permissions, req.justification.clone(), )?; let env = attempt .env_for(&spec) .map_err(|err| ToolError::Codex(err.into()))?; let out = execute_env(&env, attempt.policy, Self::stdout_stream(ctx)) .await .map_err(ToolError::Codex)?; Ok(out) } }