use crate::can_request_original_image_detail; use codex_features::Feature; use codex_features::Features; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::PathBuf; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { Classic, ZshFork, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ToolUserShellType { Zsh, Bash, PowerShell, Sh, Cmd, } pub fn request_user_input_available_modes(features: &Features) -> Vec { TUI_VISIBLE_COLLABORATION_MODES .into_iter() .filter(|mode| { mode.allows_request_user_input() || (features.enabled(Feature::DefaultModeRequestUserInput) && *mode == ModeKind::Default) }) .collect() } #[derive(Debug, Clone, Eq, PartialEq)] pub enum UnifiedExecShellMode { Direct, ZshFork(ZshForkConfig), } #[derive(Debug, Clone, Eq, PartialEq)] pub struct ZshForkConfig { pub shell_zsh_path: AbsolutePathBuf, pub main_execve_wrapper_exe: AbsolutePathBuf, } impl UnifiedExecShellMode { pub fn for_session( shell_command_backend: ShellCommandBackendConfig, user_shell_type: ToolUserShellType, shell_zsh_path: Option<&PathBuf>, main_execve_wrapper_exe: Option<&PathBuf>, ) -> Self { if cfg!(unix) && shell_command_backend == ShellCommandBackendConfig::ZshFork && matches!(user_shell_type, ToolUserShellType::Zsh) && let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) = (shell_zsh_path, main_execve_wrapper_exe) && let (Ok(shell_zsh_path), Ok(main_execve_wrapper_exe)) = ( AbsolutePathBuf::try_from(shell_zsh_path.as_path()).inspect_err(|err| { tracing::warn!( "Failed to convert shell_zsh_path `{shell_zsh_path:?}`: {err:?}" ) }), AbsolutePathBuf::try_from(main_execve_wrapper_exe.as_path()).inspect_err( |err| { tracing::warn!( "Failed to convert main_execve_wrapper_exe `{main_execve_wrapper_exe:?}`: {err:?}" ) }, ), ) { Self::ZshFork(ZshForkConfig { shell_zsh_path, main_execve_wrapper_exe, }) } else { Self::Direct } } } #[derive(Debug, Clone)] pub struct ToolsConfig { pub available_models: Vec, pub shell_type: ConfigShellToolType, pub shell_command_backend: ShellCommandBackendConfig, pub unified_exec_shell_mode: UnifiedExecShellMode, pub environment_mode: ToolEnvironmentMode, pub allow_login_shell: bool, pub apply_patch_tool_type: Option, pub web_search_mode: Option, pub web_search_config: Option, pub web_search_tool_type: WebSearchToolType, pub image_gen_tool: bool, pub search_tool: bool, pub namespace_tools: bool, pub tool_suggest: bool, pub exec_permission_approvals_enabled: bool, pub request_permissions_tool_enabled: bool, pub code_mode_enabled: bool, pub code_mode_only_enabled: bool, pub can_request_original_image_detail: bool, pub collab_tools: bool, pub goal_tools: bool, pub multi_agent_v2: bool, pub multi_agent_v2_non_code_mode_only: bool, pub hide_spawn_agent_metadata: bool, pub spawn_agent_usage_hint: bool, pub spawn_agent_usage_hint_text: Option, pub max_concurrent_threads_per_session: Option, pub wait_agent_min_timeout_ms: Option, pub request_user_input_available_modes: Vec, pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, pub agent_type_description: String, } pub struct ToolsConfigParams<'a> { pub model_info: &'a ModelInfo, pub available_models: &'a [ModelPreset], pub features: &'a Features, pub image_generation_tool_auth_allowed: bool, pub web_search_mode: Option, pub session_source: SessionSource, pub permission_profile: &'a PermissionProfile, pub windows_sandbox_level: WindowsSandboxLevel, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolEnvironmentMode { None, Single, Multiple, } impl ToolEnvironmentMode { pub fn from_count(count: usize) -> Self { match count { 0 => Self::None, 1 => Self::Single, _ => Self::Multiple, } } pub fn has_environment(self) -> bool { !matches!(self, Self::None) } } impl ToolsConfig { pub fn new(params: &ToolsConfigParams<'_>) -> Self { let ToolsConfigParams { model_info, available_models, features, image_generation_tool_auth_allowed, web_search_mode, session_source, .. } = params; let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform); let include_code_mode = features.enabled(Feature::CodeMode); let include_code_mode_only = include_code_mode && features.enabled(Feature::CodeModeOnly); let include_goal_tools = features.enabled(Feature::Goals); let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2); let include_collab_tools = include_multi_agent_v2 || features.enabled(Feature::Collab); let include_agent_jobs = features.enabled(Feature::SpawnCsv); let include_search_tool = model_info.supports_search_tool && features.enabled(Feature::ToolSearch); let include_tool_suggest = features.enabled(Feature::ToolSuggest) && features.enabled(Feature::Apps) && features.enabled(Feature::Plugins); let include_original_image_detail = can_request_original_image_detail(model_info); // API-key auth bypasses Codex backend entitlement/tool normalization, so // callers must confirm ChatGPT auth before exposing the built-in tool. let include_image_gen_tool = *image_generation_tool_auth_allowed && features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info); let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals); let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool); let shell_command_backend = if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) { ShellCommandBackendConfig::ZshFork } else { ShellCommandBackendConfig::Classic }; let unified_exec_enabled = features.enabled(Feature::UnifiedExec); let model_shell_type = match model_info.shell_type { ConfigShellToolType::UnifiedExec if !unified_exec_enabled => { ConfigShellToolType::ShellCommand } ConfigShellToolType::Default | ConfigShellToolType::Local => { ConfigShellToolType::ShellCommand } other => other, }; let shell_type = if !features.enabled(Feature::ShellTool) { ConfigShellToolType::Disabled } else if features.enabled(Feature::ShellZshFork) { ConfigShellToolType::ShellCommand } else if unified_exec_enabled { if codex_utils_pty::conpty_supported() { ConfigShellToolType::UnifiedExec } else { ConfigShellToolType::ShellCommand } } else { model_shell_type }; let apply_patch_tool_type = model_info .apply_patch_tool_type .clone() .or_else(|| include_apply_patch_tool.then_some(ApplyPatchToolType::Freeform)); let agent_jobs_worker_tools = include_agent_jobs && matches!( session_source, SessionSource::SubAgent(SubAgentSource::Other(label)) if label.starts_with("agent_job:") ); Self { available_models: available_models.to_vec(), shell_type, shell_command_backend, unified_exec_shell_mode: UnifiedExecShellMode::Direct, environment_mode: ToolEnvironmentMode::Single, allow_login_shell: true, apply_patch_tool_type, web_search_mode: *web_search_mode, web_search_config: None, web_search_tool_type: model_info.web_search_tool_type, image_gen_tool: include_image_gen_tool, search_tool: include_search_tool, namespace_tools: true, tool_suggest: include_tool_suggest, exec_permission_approvals_enabled, request_permissions_tool_enabled, code_mode_enabled: include_code_mode, code_mode_only_enabled: include_code_mode_only, can_request_original_image_detail: include_original_image_detail, collab_tools: include_collab_tools, goal_tools: include_goal_tools, multi_agent_v2: include_multi_agent_v2, multi_agent_v2_non_code_mode_only: false, hide_spawn_agent_metadata: false, spawn_agent_usage_hint: true, spawn_agent_usage_hint_text: None, max_concurrent_threads_per_session: None, wait_agent_min_timeout_ms: None, request_user_input_available_modes: request_user_input_available_modes(features), experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, agent_type_description: String::new(), } } pub fn with_agent_type_description(mut self, agent_type_description: String) -> Self { self.agent_type_description = agent_type_description; self } pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self { if !namespace_tools { self.namespace_tools = false; } self } pub fn with_image_generation_capability(mut self, image_generation: bool) -> Self { if !image_generation { self.image_gen_tool = false; } self } pub fn with_web_search_capability(mut self, web_search: bool) -> Self { if !web_search { self.web_search_mode = None; } self } pub fn with_spawn_agent_usage_hint(mut self, spawn_agent_usage_hint: bool) -> Self { self.spawn_agent_usage_hint = spawn_agent_usage_hint; self } pub fn with_spawn_agent_usage_hint_text( mut self, spawn_agent_usage_hint_text: Option, ) -> Self { self.spawn_agent_usage_hint_text = spawn_agent_usage_hint_text; self } pub fn with_hide_spawn_agent_metadata(mut self, hide_spawn_agent_metadata: bool) -> Self { self.hide_spawn_agent_metadata = hide_spawn_agent_metadata; self } pub fn with_multi_agent_v2_non_code_mode_only( mut self, multi_agent_v2_non_code_mode_only: bool, ) -> Self { self.multi_agent_v2_non_code_mode_only = self.multi_agent_v2 && multi_agent_v2_non_code_mode_only; self } pub fn with_goal_tools_allowed(mut self, allowed: bool) -> Self { self.goal_tools = self.goal_tools && allowed; self } pub fn with_max_concurrent_threads_per_session( mut self, max_concurrent_threads_per_session: Option, ) -> Self { self.max_concurrent_threads_per_session = max_concurrent_threads_per_session; self } pub fn with_wait_agent_min_timeout_ms( mut self, wait_agent_min_timeout_ms: Option, ) -> Self { self.wait_agent_min_timeout_ms = wait_agent_min_timeout_ms; self } pub fn with_allow_login_shell(mut self, allow_login_shell: bool) -> Self { self.allow_login_shell = allow_login_shell; self } pub fn with_environment_mode(mut self, environment_mode: ToolEnvironmentMode) -> Self { self.environment_mode = environment_mode; self } pub fn with_unified_exec_shell_mode( mut self, unified_exec_shell_mode: UnifiedExecShellMode, ) -> Self { self.unified_exec_shell_mode = unified_exec_shell_mode; self } pub fn with_unified_exec_shell_mode_for_session( mut self, user_shell_type: ToolUserShellType, shell_zsh_path: Option<&PathBuf>, main_execve_wrapper_exe: Option<&PathBuf>, ) -> Self { self.unified_exec_shell_mode = UnifiedExecShellMode::for_session( self.shell_command_backend, user_shell_type, shell_zsh_path, main_execve_wrapper_exe, ); self } pub fn with_web_search_config(mut self, web_search_config: Option) -> Self { self.web_search_config = web_search_config; self } pub fn for_code_mode_nested_tools(&self) -> Self { let mut nested = self.clone(); nested.code_mode_enabled = false; nested.code_mode_only_enabled = false; nested } } fn supports_image_generation(model_info: &ModelInfo) -> bool { model_info.input_modalities.contains(&InputModality::Image) } #[cfg(test)] #[path = "tool_config_tests.rs"] mod tests;