use codex_protocol::ThreadId;use codex_protocol::models::ShellCommandToolCallParams;use codex_tools::ShellCommandBackendConfig;use codex_tools::ToolName;use crate::exec::ExecCapturePolicy;use crate::exec::ExecParams;use crate::exec_env::create_env;use crate::function_tool::FunctionCallError;use crate::maybe_emit_implicit_skill_invocation;use crate::session::turn_context::TurnContext;use crate::shell::Shell;use crate::tools::context::ToolInvocation;use crate::tools::context::ToolPayload;use crate::tools::context::boxed_tool_output;use crate::tools::handlers::parse_arguments_with_base_path;use crate::tools::handlers::resolve_workdir_base_path;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::tools::runtimes::shell::ShellRuntimeBackend;use codex_tools::ToolSpec;use super::super::shell_spec::CommandToolOptions;use super::super::shell_spec::create_shell_command_tool;use super::RunExecLikeArgs;use super::run_exec_like;use super::shell_command_payload_command;#[derive(Clone, Copy, Debug, Eq, PartialEq)]enum ShellCommandBackend { Classic, ZshFork,}pub struct ShellCommandHandler { backend: ShellCommandBackend, options: ShellCommandHandlerOptions,}#[derive(Clone, Copy)]pub(crate) struct ShellCommandHandlerOptions { pub(crate) backend_config: ShellCommandBackendConfig, pub(crate) allow_login_shell: bool, pub(crate) exec_permission_approvals_enabled: bool,}impl ShellCommandHandler { pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self { let backend = match options.backend_config { ShellCommandBackendConfig::Classic => ShellCommandBackend::Classic, ShellCommandBackendConfig::ZshFork => ShellCommandBackend::ZshFork, }; Self { backend, options } } fn shell_runtime_backend(&self) -> ShellRuntimeBackend { match self.backend { ShellCommandBackend::Classic => ShellRuntimeBackend::ShellCommandClassic, ShellCommandBackend::ZshFork => ShellRuntimeBackend::ShellCommandZshFork, } } pub(super) fn resolve_use_login_shell( login: Option<bool>, allow_login_shell: bool, ) -> Result<bool, FunctionCallError> { if !allow_login_shell && login == Some(true) { return Err(FunctionCallError::RespondToModel( "login shell is disabled by config; omit `login` or set it to false.".to_string(), )); } Ok(login.unwrap_or(allow_login_shell)) } pub(super) fn base_command(shell: &Shell, command: &str, use_login_shell: bool) -> Vec<String> { shell.derive_exec_args(command, use_login_shell) } pub(super) fn to_exec_params( params: &ShellCommandToolCallParams, session: &crate::session::session::Session, turn_context: &TurnContext, thread_id: ThreadId, allow_login_shell: bool, ) -> Result<ExecParams, FunctionCallError> { let shell = session.user_shell(); let use_login_shell = Self::resolve_use_login_shell(params.login, allow_login_shell)?; let command = Self::base_command(shell.as_ref(), ¶ms.command, use_login_shell); #[allow(deprecated)] let cwd = turn_context.resolve_path(params.workdir.clone()); Ok(ExecParams { command, cwd, expiration: params.timeout_ms.into(), capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config .permissions .windows_sandbox_private_desktop, justification: params.justification.clone(), arg0: None, }) }}impl From<ShellCommandBackendConfig> for ShellCommandHandler { fn from(backend_config: ShellCommandBackendConfig) -> Self { Self::new(ShellCommandHandlerOptions { backend_config, allow_login_shell: false, exec_permission_approvals_enabled: false, }) }}impl ToolExecutor<ToolInvocation> for ShellCommandHandler { fn tool_name(&self) -> ToolName { ToolName::plain("shell_command") } fn spec(&self) -> ToolSpec { create_shell_command_tool(CommandToolOptions { allow_login_shell: self.options.allow_login_shell, exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled, }) } fn supports_parallel_tool_calls(&self) -> bool { true } fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> { Box::pin(self.handle_call(invocation)) }}impl ShellCommandHandler { async fn handle_call( &self, invocation: ToolInvocation, ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> { let ToolInvocation { session, turn, cancellation_token, tracker, call_id, payload, .. } = invocation; let tool_name = self.tool_name(); let ToolPayload::Function { arguments } = payload else { return Err(FunctionCallError::RespondToModel(format!( "unsupported payload for shell_command handler: {tool_name}" ))); }; #[allow(deprecated)] let cwd = resolve_workdir_base_path(&arguments, &turn.cwd)?; let params: ShellCommandToolCallParams = parse_arguments_with_base_path(&arguments, &cwd)?; #[allow(deprecated)] let workdir = turn.resolve_path(params.workdir.clone()); maybe_emit_implicit_skill_invocation( session.as_ref(), turn.as_ref(), ¶ms.command, &workdir, ) .await; let prefix_rule = params.prefix_rule.clone(); let exec_params = Self::to_exec_params( ¶ms, session.as_ref(), turn.as_ref(), session.thread_id, turn.config.permissions.allow_login_shell, )?; let shell_type = Some(session.user_shell().shell_type); run_exec_like(RunExecLikeArgs { tool_name, exec_params, cancellation_token, hook_command: params.command, shell_type, additional_permissions: params.additional_permissions.clone(), prefix_rule, session, turn, tracker, call_id, shell_runtime_backend: self.shell_runtime_backend(), }) .await .map(boxed_tool_output) }}impl CoreToolRuntime for ShellCommandHandler { fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } fn waits_for_runtime_cancellation(&self) -> bool { true } fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> { shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload { tool_name: HookToolName::bash(), tool_input: serde_json::json!({ "command": command }), }) } 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 shell_command payload".to_string(), )); }; invocation.payload = ToolPayload::Function { arguments: rewrite_function_string_argument( &arguments, "shell_command", "command", updated_hook_command(&updated_input)?, )?, }; Ok(invocation) } fn post_tool_use_payload( &self, invocation: &ToolInvocation, result: &dyn crate::tools::context::ToolOutput, ) -> Option<PostToolUsePayload> { let tool_response = result.post_tool_use_response(&invocation.call_id, &invocation.payload)?; let command = shell_command_payload_command(&invocation.payload)?; Some(PostToolUsePayload { tool_name: HookToolName::bash(), tool_use_id: invocation.call_id.clone(), tool_input: serde_json::json!({ "command": command }), tool_response, }) }}