use std::sync::Arc;use crate::function_tool::FunctionCallError;use crate::maybe_emit_implicit_skill_invocation;use crate::tools::context::ExecCommandToolOutput;use crate::tools::context::ToolInvocation;use crate::tools::context::ToolPayload;use crate::tools::context::boxed_tool_output;use crate::tools::handlers::apply_granted_turn_permissions;use crate::tools::handlers::apply_patch::intercept_apply_patch;use crate::tools::handlers::implicit_granted_permissions;use crate::tools::handlers::normalize_and_validate_additional_permissions;use crate::tools::handlers::parse_arguments;use crate::tools::handlers::parse_arguments_with_base_path;use crate::tools::handlers::resolve_tool_environment;use crate::tools::handlers::rewrite_function_string_argument;use crate::tools::handlers::updated_hook_command;use crate::tools::hook_names::HookToolName;use crate::tools::registry::CoreToolRuntime;use crate::tools::registry::PostToolUsePayload;use crate::tools::registry::PreToolUsePayload;use crate::tools::registry::ToolExecutor;use crate::unified_exec::ExecCommandRequest;use crate::unified_exec::UnifiedExecContext;use crate::unified_exec::UnifiedExecError;use crate::unified_exec::UnifiedExecProcessManager;use crate::unified_exec::generate_chunk_id;use codex_features::Feature;use codex_otel::SessionTelemetry;use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;use codex_tools::ToolName;use codex_tools::ToolSpec;use codex_utils_output_truncation::approx_token_count;use super::super::shell_spec::CommandToolOptions;use super::super::shell_spec::create_exec_command_tool_with_environment_id;use super::ExecCommandArgs;use super::ExecCommandEnvironmentArgs;use super::get_command;use super::post_unified_exec_tool_use_payload;use super::shell_mode_for_environment;#[derive(Clone, Copy)]pub(crate) struct ExecCommandHandlerOptions { pub(crate) allow_login_shell: bool, pub(crate) exec_permission_approvals_enabled: bool, pub(crate) include_environment_id: bool, pub(crate) include_shell_parameter: bool,}pub struct ExecCommandHandler { options: ExecCommandHandlerOptions,}impl Default for ExecCommandHandler { fn default() -> Self { Self { options: ExecCommandHandlerOptions { allow_login_shell: false, exec_permission_approvals_enabled: false, include_environment_id: false, include_shell_parameter: true, }, } }}impl ExecCommandHandler { pub(crate) fn new(options: ExecCommandHandlerOptions) -> Self { Self { options } }}impl ToolExecutor<ToolInvocation> for ExecCommandHandler { fn tool_name(&self) -> ToolName { ToolName::plain("exec_command") } fn spec(&self) -> ToolSpec { create_exec_command_tool_with_environment_id( CommandToolOptions { allow_login_shell: self.options.allow_login_shell, exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled, }, self.options.include_environment_id, self.options.include_shell_parameter, ) } fn supports_parallel_tool_calls(&self) -> bool { true } fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> { Box::pin(self.handle_call(invocation)) }}impl ExecCommandHandler { async fn handle_call( &self, invocation: ToolInvocation, ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> { let ToolInvocation { session, turn, tracker, call_id, payload, .. } = invocation; let arguments = match payload { ToolPayload::Function { arguments } => arguments, _ => { return Err(FunctionCallError::RespondToModel( "exec_command handler received unsupported payload".to_string(), )); } }; let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager; let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone()); let environment_args: ExecCommandEnvironmentArgs = parse_arguments(&arguments)?; let Some(turn_environment) = resolve_tool_environment(turn.as_ref(), environment_args.environment_id.as_deref())? else { return Err(FunctionCallError::RespondToModel( "unified exec is unavailable in this session".to_string(), )); }; // TODO(anp): Resolve tool paths using the selected environment's native path convention // so unified exec can support relative paths in foreign environments. let native_environment_cwd = turn_environment.cwd().to_abs_path().map_err(|err| { FunctionCallError::RespondToModel(format!( "environment cwd `{}` is not native to the Codex host: {err}", turn_environment.cwd() )) })?; let cwd = environment_args .workdir .as_deref() .filter(|workdir| !workdir.is_empty()) .map_or_else( || native_environment_cwd.clone(), |workdir| native_environment_cwd.join(workdir), ); let environment = Arc::clone(&turn_environment.environment); let fs = environment.get_filesystem(); let args: ExecCommandArgs = parse_arguments_with_base_path(&arguments, &cwd)?; let hook_command = args.cmd.clone(); maybe_emit_implicit_skill_invocation( session.as_ref(), context.turn.as_ref(), &hook_command, &cwd, ) .await; let process_id = manager.allocate_process_id().await; let shell_mode = shell_mode_for_environment(&turn.unified_exec_shell_mode, environment.as_ref()); // Remote environments may use a different OS and must build commands with their native // shell; fall back to the session shell when the environment did not report one. let shell = turn_environment .shell .clone() .map(Arc::new) .unwrap_or_else(|| session.user_shell()); let resolved_command = get_command( &args, shell, &shell_mode, turn.config.permissions.allow_login_shell, ) .map_err(FunctionCallError::RespondToModel)?; let command = resolved_command.command; let shell_type = resolved_command.shell_type; let command_for_display = codex_shell_command::parse_command::shlex_join(&command); let ExecCommandArgs { tty, yield_time_ms, max_output_tokens, sandbox_permissions, additional_permissions, justification, prefix_rule, .. } = args; let exec_permission_approvals_enabled = session.features().enabled(Feature::ExecPermissionApprovals); let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( context.session.as_ref(), &turn_environment.environment_id, cwd.as_path(), sandbox_permissions, additional_permissions, ) .await; let additional_permissions_allowed = exec_permission_approvals_enabled || (session.features().enabled(Feature::RequestPermissionsTool) && effective_additional_permissions.permissions_preapproved); // Sticky turn permissions have already been approved, so they should // continue through the normal exec approval flow for the command. if effective_additional_permissions .sandbox_permissions .requests_sandbox_override() && !effective_additional_permissions.permissions_preapproved && !matches!( context.turn.approval_policy.value(), codex_protocol::protocol::AskForApproval::OnRequest ) { let approval_policy = context.turn.approval_policy.value(); manager.release_process_id(process_id).await; return Err(FunctionCallError::RespondToModel(format!( "approval policy is {approval_policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {approval_policy:?}" ))); } let normalized_additional_permissions = match implicit_granted_permissions( sandbox_permissions, requested_additional_permissions.as_ref(), &effective_additional_permissions, ) .map_or_else( || { normalize_and_validate_additional_permissions( additional_permissions_allowed, context.turn.approval_policy.value(), effective_additional_permissions.sandbox_permissions, effective_additional_permissions.additional_permissions, effective_additional_permissions.permissions_preapproved, &cwd, ) }, |permissions| Ok(Some(permissions)), ) { Ok(normalized) => normalized, Err(err) => { manager.release_process_id(process_id).await; return Err(FunctionCallError::RespondToModel(err)); } }; if let Some(output) = intercept_apply_patch( &command, &cwd, fs.as_ref(), turn_environment.clone(), context.session.clone(), context.turn.clone(), Some(&tracker), &context.call_id, "exec_command", ) .await? { manager.release_process_id(process_id).await; return Ok(boxed_tool_output(ExecCommandToolOutput { event_call_id: String::new(), chunk_id: String::new(), wall_time: std::time::Duration::ZERO, raw_output: output.into_text().into_bytes(), truncation_policy: turn.truncation_policy, max_output_tokens, process_id: None, exit_code: None, original_token_count: None, hook_command: None, })); } emit_unified_exec_tty_metric(&turn.session_telemetry, tty); match manager .exec_command( ExecCommandRequest { command, shell_type, hook_command: hook_command.clone(), process_id, yield_time_ms, max_output_tokens, cwd, sandbox_cwd: native_environment_cwd, turn_environment: turn_environment.clone(), shell_mode, network: context.turn.network.clone(), tty, sandbox_permissions: effective_additional_permissions.sandbox_permissions, additional_permissions: normalized_additional_permissions, additional_permissions_preapproved: effective_additional_permissions .permissions_preapproved, justification, prefix_rule, }, &context, ) .await { Ok(response) => Ok(boxed_tool_output(response)), Err(UnifiedExecError::SandboxDenied { output, .. }) => { let output_text = output.aggregated_output.text; let original_token_count = approx_token_count(&output_text); Ok(boxed_tool_output(ExecCommandToolOutput { event_call_id: context.call_id.clone(), chunk_id: generate_chunk_id(), wall_time: output.duration, raw_output: output_text.into_bytes(), truncation_policy: turn.truncation_policy, max_output_tokens, // Sandbox denial is terminal, so there is no live // process for write_stdin to resume. process_id: None, exit_code: Some(output.exit_code), original_token_count: Some(original_token_count), hook_command: Some(hook_command), })) } Err(err) => Err(FunctionCallError::RespondToModel(format!( "exec_command failed for `{command_for_display}`: {err:?}" ))), } }}impl CoreToolRuntime for ExecCommandHandler { fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> { let ToolPayload::Function { arguments } = &invocation.payload else { return None; }; parse_arguments::<ExecCommandArgs>(arguments) .ok() .map(|args| PreToolUsePayload { tool_name: HookToolName::bash(), tool_input: serde_json::json!({ "command": args.cmd }), }) } fn with_updated_hook_input( &self, mut invocation: ToolInvocation, updated_input: serde_json::Value, ) -> Result<ToolInvocation, FunctionCallError> { let ToolPayload::Function { arguments } = invocation.payload else { return Err(FunctionCallError::RespondToModel( "hook input rewrite received unsupported exec_command payload".to_string(), )); }; invocation.payload = ToolPayload::Function { arguments: rewrite_function_string_argument( &arguments, "exec_command", "cmd", updated_hook_command(&updated_input)?, )?, }; Ok(invocation) } fn post_tool_use_payload( &self, invocation: &ToolInvocation, result: &dyn crate::tools::context::ToolOutput, ) -> Option<PostToolUsePayload> { post_unified_exec_tool_use_payload(invocation, result) }}fn emit_unified_exec_tty_metric(session_telemetry: &SessionTelemetry, tty: bool) { session_telemetry.counter( TOOL_CALL_UNIFIED_EXEC_METRIC, /*inc*/ 1, &[("tty", if tty { "true" } else { "false" })], );}