Codex Handbook
core/src/tools/handlers/shell/shell_command.rs 262 lines
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(), &params.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(),            &params.command,            &workdir,        )        .await;        let prefix_rule = params.prefix_rule.clone();        let exec_params = Self::to_exec_params(            &params,            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,        })    }}