Remove ToolsConfig from tool planning (#22835)

## Why

`codex-tools` is meant to hold reusable tool primitives, but
`ToolsConfig` had become a second copy of core runtime decisions instead
of a small shared contract. It carried provider capabilities, auth/model
gates, permission and environment state, web/search/image feature gates,
multi-agent settings, and goal availability from core into `codex-tools`
([definition](https://github.com/openai/codex/blob/22dd9ad3929253ed24d7ee4f10f238e95ab25f37/codex-rs/tools/src/tool_config.rs#L97),
[stored on each
`TurnContext`](https://github.com/openai/codex/blob/22dd9ad3929253ed24d7ee4f10f238e95ab25f37/codex-rs/core/src/session/turn_context.rs#L87)).
Every session/context variant then had to build and mutate that snapshot
before assembling tools.

This PR removes that master object instead of renaming it. Tool planning
now reads the live `TurnContext`, where `codex-core` already owns those
decisions, while `codex-tools` keeps only reusable primitives and a
generic `ToolSetBuilder`/`ToolSet` accumulator.

## What Changed

- Removed `ToolsConfig` / `ToolsConfigParams` from `codex-tools`; the
crate keeps the shared helpers that still belong there, including
request-user-input mode selection, shell backend/type resolution,
`UnifiedExecShellMode`, and `ToolEnvironmentMode`.
- Replaced config-snapshot planning with `ToolRouter::from_turn_context`
and a `spec_plan` pipeline over `CoreToolPlanContext`, deriving provider
capabilities, auth gates, model support, feature gates, environment
count, goal support, multi-agent options, web search, and image
generation from the authoritative turn state.
- Added generic `codex_tools::ToolSetBuilder` / `ToolSet`, plus the
small core adapter needed to accumulate `CoreToolRuntime` values and
hosted model specs.
- Added the `tool_family::shell` registration module and moved
shell/unified-exec/memory accounting call sites to read the narrow
per-turn fields directly.
- Narrowed `TurnContext` to the remaining explicit per-turn fields
needed by planning: `available_models`, `unified_exec_shell_mode`, and
`goal_tools_supported`.
- Reworked MCP exposure and tool-search setup so deferred/direct MCP
behavior is driven by the current turn rather than a precomputed config
snapshot.
- Replaced the large expected-spec fixture tests with focused
behavior-level coverage for shell tools, environments, goal and
agent-job gates, MCP direct/deferred exposure, tool search,
request-plugin-install, code mode, multi-agent mode, hosted tools, and
extension executor dispatch.

## Verification

- `cargo check -p codex-tools`
- `cargo check -p codex-core --lib`
- `cargo test -p codex-tools`
- `cargo test -p codex-core spec_plan --lib`
- `cargo test -p codex-core router --lib`
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-19 11:24:09 +02:00
1 parent ba57aab13a
commit 05e171094d
25 files changed
+1426 -5232

No files matched your search

+38 -318
View File
@@ -1,20 +1,9 @@
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;
@@ -44,6 +33,44 @@ pub fn request_user_input_available_modes(features: &Features) -> Vec<ModeKind>
.collect()
}
pub fn shell_command_backend_for_features(features: &Features) -> ShellCommandBackendConfig {
if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
ShellCommandBackendConfig::ZshFork
} else {
ShellCommandBackendConfig::Classic
}
}
pub fn shell_type_for_model_and_features(
model_info: &ModelInfo,
features: &Features,
) -> ConfigShellToolType {
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,
};
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
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum UnifiedExecShellMode {
Direct,
@@ -93,57 +120,6 @@ impl UnifiedExecShellMode {
}
}
#[derive(Debug, Clone)]
pub struct ToolsConfig {
pub available_models: Vec<ModelPreset>,
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<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
pub web_search_config: Option<WebSearchConfig>,
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<String>,
pub multi_agent_v2_tool_namespace: Option<String>,
pub max_concurrent_threads_per_session: Option<usize>,
pub wait_agent_min_timeout_ms: Option<i64>,
pub wait_agent_max_timeout_ms: Option<i64>,
pub wait_agent_default_timeout_ms: Option<i64>,
pub request_user_input_available_modes: Vec<ModeKind>,
pub experimental_supported_tools: Vec<String>,
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<WebSearchMode>,
pub session_source: SessionSource,
pub permission_profile: &'a PermissionProfile,
pub windows_sandbox_level: WindowsSandboxLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolEnvironmentMode {
None,
@@ -165,262 +141,6 @@ impl ToolEnvironmentMode {
}
}
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_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;
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();
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,
multi_agent_v2_tool_namespace: None,
max_concurrent_threads_per_session: None,
wait_agent_min_timeout_ms: None,
wait_agent_max_timeout_ms: None,
wait_agent_default_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<String>,
) -> 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_tool_namespace(
mut self,
multi_agent_v2_tool_namespace: Option<String>,
) -> Self {
self.multi_agent_v2_tool_namespace = multi_agent_v2_tool_namespace;
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<usize>,
) -> 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<i64>,
) -> Self {
self.wait_agent_min_timeout_ms = wait_agent_min_timeout_ms;
self
}
pub fn with_wait_agent_max_timeout_ms(
mut self,
wait_agent_max_timeout_ms: Option<i64>,
) -> Self {
self.wait_agent_max_timeout_ms = wait_agent_max_timeout_ms;
self
}
pub fn with_wait_agent_default_timeout_ms(
mut self,
wait_agent_default_timeout_ms: Option<i64>,
) -> Self {
self.wait_agent_default_timeout_ms = wait_agent_default_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<WebSearchConfig>) -> 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;