Skip to main content

codex_tools/
tool_config.rs

1use codex_features::Feature;
2use codex_features::Features;
3use codex_protocol::config_types::ModeKind;
4use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
5use codex_protocol::openai_models::ConfigShellToolType;
6use codex_protocol::openai_models::ModelInfo;
7use codex_utils_absolute_path::AbsolutePathBuf;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Copy, Eq, PartialEq)]
11pub enum ShellCommandBackendConfig {
12    Classic,
13    ZshFork,
14}
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq)]
17pub enum UnifiedExecFeatureMode {
18    /// Unified exec should not be selected by this feature set.
19    ///
20    /// This includes standalone `shell_zsh_fork`: until
21    /// `unified_exec_zsh_fork` is enabled too, `shell_zsh_fork` keeps using
22    /// the shell command backend instead of silently opting unified exec into
23    /// zsh-fork interception.
24    Disabled,
25    Direct,
26    ZshFork,
27}
28
29#[derive(Debug, Clone, Copy, Eq, PartialEq)]
30pub enum ToolUserShellType {
31    Zsh,
32    Bash,
33    PowerShell,
34    Sh,
35    Cmd,
36}
37
38pub fn request_user_input_available_modes(features: &Features) -> Vec<ModeKind> {
39    TUI_VISIBLE_COLLABORATION_MODES
40        .into_iter()
41        .filter(|mode| {
42            mode.allows_request_user_input()
43                || (features.enabled(Feature::DefaultModeRequestUserInput)
44                    && *mode == ModeKind::Default)
45        })
46        .collect()
47}
48
49pub fn shell_command_backend_for_features(features: &Features) -> ShellCommandBackendConfig {
50    if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
51        ShellCommandBackendConfig::ZshFork
52    } else {
53        ShellCommandBackendConfig::Classic
54    }
55}
56
57/// Returns the unified-exec mode requested by feature policy, before runtime
58/// session inputs such as platform, user shell, and zsh-fork binary paths are
59/// resolved.
60///
61/// `unified_exec_zsh_fork` is only a composition gate. It does not enable
62/// either underlying shell mode on its own, so disabling `unified_exec` or
63/// `shell_zsh_fork` keeps those features independently off. This lets
64/// enterprise deployments opt into, or out of, unified exec and zsh-fork
65/// behavior separately; otherwise enabling the composition flag would silently
66/// activate a shell backend that the configured feature set left disabled.
67pub fn unified_exec_feature_mode_for_features(features: &Features) -> UnifiedExecFeatureMode {
68    if !features.enabled(Feature::ShellTool) || !features.enabled(Feature::UnifiedExec) {
69        UnifiedExecFeatureMode::Disabled
70    } else if features.enabled(Feature::ShellZshFork) {
71        if features.enabled(Feature::UnifiedExecZshFork) {
72            UnifiedExecFeatureMode::ZshFork
73        } else {
74            UnifiedExecFeatureMode::Disabled
75        }
76    } else {
77        UnifiedExecFeatureMode::Direct
78    }
79}
80
81pub fn shell_type_for_model_and_features(
82    model_info: &ModelInfo,
83    features: &Features,
84) -> ConfigShellToolType {
85    let unified_exec_feature_mode = unified_exec_feature_mode_for_features(features);
86    let unified_exec_disabled =
87        matches!(unified_exec_feature_mode, UnifiedExecFeatureMode::Disabled);
88    let model_shell_type = match model_info.shell_type {
89        ConfigShellToolType::UnifiedExec if unified_exec_disabled => {
90            ConfigShellToolType::ShellCommand
91        }
92        ConfigShellToolType::Default | ConfigShellToolType::Local => {
93            ConfigShellToolType::ShellCommand
94        }
95        other => other,
96    };
97    let shell_command_type = match shell_command_backend_for_features(features) {
98        ShellCommandBackendConfig::Classic => model_shell_type,
99        ShellCommandBackendConfig::ZshFork => ConfigShellToolType::ShellCommand,
100    };
101
102    if !features.enabled(Feature::ShellTool) {
103        ConfigShellToolType::Disabled
104    } else {
105        match unified_exec_feature_mode {
106            UnifiedExecFeatureMode::Disabled => shell_command_type,
107            UnifiedExecFeatureMode::Direct | UnifiedExecFeatureMode::ZshFork => {
108                if codex_utils_pty::conpty_supported() {
109                    ConfigShellToolType::UnifiedExec
110                } else {
111                    ConfigShellToolType::ShellCommand
112                }
113            }
114        }
115    }
116}
117
118#[derive(Debug, Clone, Eq, PartialEq)]
119pub enum UnifiedExecShellMode {
120    Direct,
121    ZshFork(ZshForkConfig),
122}
123
124#[derive(Debug, Clone, Eq, PartialEq)]
125pub struct ZshForkConfig {
126    pub shell_zsh_path: AbsolutePathBuf,
127    pub main_execve_wrapper_exe: AbsolutePathBuf,
128}
129
130impl UnifiedExecShellMode {
131    pub fn for_session(
132        feature_mode: UnifiedExecFeatureMode,
133        user_shell_type: ToolUserShellType,
134        shell_zsh_path: Option<&PathBuf>,
135        main_execve_wrapper_exe: Option<&PathBuf>,
136    ) -> Self {
137        if cfg!(unix)
138            && matches!(feature_mode, UnifiedExecFeatureMode::ZshFork)
139            && matches!(user_shell_type, ToolUserShellType::Zsh)
140            && let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) =
141                (shell_zsh_path, main_execve_wrapper_exe)
142            && let (Ok(shell_zsh_path), Ok(main_execve_wrapper_exe)) = (
143                AbsolutePathBuf::try_from(shell_zsh_path.as_path()).inspect_err(|err| {
144                    tracing::warn!(
145                        "Failed to convert shell_zsh_path `{shell_zsh_path:?}`: {err:?}"
146                    )
147                }),
148                AbsolutePathBuf::try_from(main_execve_wrapper_exe.as_path()).inspect_err(
149                    |err| {
150                        tracing::warn!(
151                            "Failed to convert main_execve_wrapper_exe `{main_execve_wrapper_exe:?}`: {err:?}"
152                        )
153                    },
154                ),
155            )
156        {
157            Self::ZshFork(ZshForkConfig {
158                shell_zsh_path,
159                main_execve_wrapper_exe,
160            })
161        } else {
162            Self::Direct
163        }
164    }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum ToolEnvironmentMode {
169    None,
170    Single,
171    Multiple,
172}
173
174impl ToolEnvironmentMode {
175    pub fn from_count(count: usize) -> Self {
176        match count {
177            0 => Self::None,
178            1 => Self::Single,
179            _ => Self::Multiple,
180        }
181    }
182
183    pub fn has_environment(self) -> bool {
184        !matches!(self, Self::None)
185    }
186}
187
188#[cfg(test)]
189#[path = "tool_config_tests.rs"]
190mod tests;