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
2026-05-19 11:24:09 +02:00
committed by GitHub
parent ba57aab13a
commit 05e171094d
25 changed files with 1427 additions and 5233 deletions
+2 -3
View File
@@ -3,7 +3,6 @@ use std::collections::HashSet;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo as McpToolInfo;
use codex_tools::ToolsConfig;
use crate::config::Config;
use crate::connectors;
@@ -19,7 +18,7 @@ pub(crate) fn build_mcp_tool_exposure(
all_mcp_tools: &[McpToolInfo],
connectors: Option<&[connectors::AppInfo]>,
config: &Config,
tools_config: &ToolsConfig,
search_tool_enabled: bool,
) -> McpToolExposure {
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
if let Some(connectors) = connectors {
@@ -30,7 +29,7 @@ pub(crate) fn build_mcp_tool_exposure(
));
}
let should_defer = tools_config.search_tool
let should_defer = search_tool_enabled
&& (config
.features
.enabled(Feature::ToolSearchAlwaysDeferMcpTools)
+7 -36
View File
@@ -2,17 +2,9 @@ use std::collections::HashSet;
use std::sync::Arc;
use codex_features::Feature;
use codex_features::Features;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_models_manager::test_support::construct_model_info_offline_for_tests;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SessionSource;
use codex_tools::ToolName;
use codex_tools::ToolsConfig;
use codex_tools::ToolsConfigParams;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use rmcp::model::Tool;
@@ -94,34 +86,14 @@ fn tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
.collect()
}
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
let config = test_config().await;
let model_info =
construct_model_info_offline_for_tests("gpt-5.4", &config.to_models_manager_config());
let features = Features::with_defaults();
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
tools_config.search_tool = search_tool;
tools_config
}
#[tokio::test]
async fn directly_exposes_small_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
let exposure =
build_mcp_tool_exposure(&mcp_tools, /*connectors*/ None, &config, &tools_config);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
);
assert_eq!(tool_names(&exposure.direct_tools), tool_names(&mcp_tools));
assert!(exposure.deferred_tools.is_none());
@@ -130,11 +102,11 @@ async fn directly_exposes_small_effective_tool_sets() {
#[tokio::test]
async fn searches_large_effective_tool_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
let exposure =
build_mcp_tool_exposure(&mcp_tools, /*connectors*/ None, &config, &tools_config);
let exposure = build_mcp_tool_exposure(
&mcp_tools, /*connectors*/ None, &config, /*search_tool_enabled*/ true,
);
assert!(exposure.direct_tools.is_empty());
let deferred_tools = exposure
@@ -151,7 +123,6 @@ async fn always_defer_feature_defers_apps_too() {
.features
.enable(Feature::ToolSearchAlwaysDeferMcpTools)
.expect("test config should allow feature update");
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = vec![
make_mcp_tool(
"rmcp",
@@ -176,7 +147,7 @@ async fn always_defer_feature_defers_apps_too() {
&mcp_tools,
Some(connectors.as_slice()),
&config,
&tools_config,
/*search_tool_enabled*/ true,
);
assert!(exposure.direct_tools.is_empty());
+6 -4
View File
@@ -43,14 +43,16 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
(None, "shell_command") => serde_json::from_str::<ShellCommandToolCallParams>(arguments)
.ok()
.map(|params| {
if !invocation.turn.tools_config.allow_login_shell && params.login == Some(true) {
if !invocation.turn.config.permissions.allow_login_shell
&& params.login == Some(true)
{
#[allow(deprecated)]
let cwd = invocation.turn.resolve_path(params.workdir).to_path_buf();
return (Vec::new(), cwd);
}
let use_login_shell = params
.login
.unwrap_or(invocation.turn.tools_config.allow_login_shell);
.unwrap_or(invocation.turn.config.permissions.allow_login_shell);
let command = invocation
.session
.user_shell()
@@ -65,8 +67,8 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
let command = crate::tools::handlers::unified_exec::get_command(
&params,
invocation.session.user_shell(),
&invocation.turn.tools_config.unified_exec_shell_mode,
invocation.turn.tools_config.allow_login_shell,
&invocation.turn.unified_exec_shell_mode,
invocation.turn.config.permissions.allow_login_shell,
)
.ok()?;
#[allow(deprecated)]
+3 -2
View File
@@ -93,6 +93,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::FileChange;
@@ -355,8 +356,8 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolsConfig;
use codex_tools::ToolsConfigParams;
use codex_tools::UnifiedExecShellMode;
use codex_tools::shell_command_backend_for_features;
use codex_utils_absolute_path::AbsolutePathBuf;
#[cfg(test)]
use codex_utils_stream_parser::ProposedPlanSegment;
+15 -54
View File
@@ -1,4 +1,3 @@
use super::turn_context::image_generation_tool_auth_allowed;
use super::*;
use std::sync::atomic::AtomicBool;
@@ -23,61 +22,21 @@ pub(super) async fn spawn_review_thread(
let mut review_features = sess.features.clone();
let _ = review_features.disable(Feature::WebSearchRequest);
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let goal_tools_supported = !config.ephemeral && parent_turn_context.tools_config.goal_tools;
let provider_capabilities = parent_turn_context.provider.capabilities();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &review_model_info,
available_models: &sess
.services
.models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await,
features: &review_features,
image_generation_tool_auth_allowed: image_generation_tool_auth_allowed(Some(
sess.services.auth_manager.as_ref(),
)),
web_search_mode: Some(review_web_search_mode),
session_source: parent_turn_context.session_source.clone(),
permission_profile: &parent_turn_context.permission_profile,
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode_for_session(
let goal_tools_supported = !config.ephemeral && parent_turn_context.goal_tools_enabled();
let available_models = sess
.services
.models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
let shell_command_backend = shell_command_backend_for_features(review_features.get());
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
shell_command_backend,
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
)
.with_web_search_config(/*web_search_config*/ None)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_environment_mode(parent_turn_context.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(goal_tools_supported)
.with_max_concurrent_threads_per_session(config.agent_max_threads)
.with_wait_agent_min_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
review_features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));
);
let review_prompt = resolved.prompt.clone();
let provider = parent_turn_context.provider.clone();
@@ -139,8 +98,10 @@ pub(super) async fn spawn_review_thread(
session_source,
thread_source: parent_turn_context.thread_source,
environments: parent_turn_context.environments.clone(),
tools_config,
features: parent_turn_context.features.clone(),
available_models,
unified_exec_shell_mode,
goal_tools_supported,
features: review_features,
ghost_snapshot: parent_turn_context.ghost_snapshot.clone(),
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
+8 -7
View File
@@ -536,8 +536,8 @@ async fn preview_session_start_hooks(
}
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: None,
@@ -9259,8 +9259,8 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
.await
};
let deferred_mcp_tools = Some(tools.clone());
let router = ToolRouter::from_config(
&turn_context.tools_config,
let router = ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
deferred_mcp_tools,
mcp_tools: Some(tools),
@@ -9828,9 +9828,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
turn_context_mut.permission_profile = PermissionProfile::Disabled;
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
let command = session
.user_shell()
.derive_exec_args(command_script, turn_context.tools_config.allow_login_shell);
let command = session.user_shell().derive_exec_args(
command_script,
turn_context.config.permissions.allow_login_shell,
);
let exec_approval_requirement = session
.services
.exec_policy
+6 -4
View File
@@ -55,6 +55,8 @@ use crate::tools::parallel::ToolCallRuntime;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::extension_tool_executors;
use crate::tools::spec_plan::search_tool_enabled;
use crate::tools::spec_plan::tool_suggest_enabled;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::turn_timing::record_turn_ttft_metric;
use crate::util::backoff;
@@ -1120,7 +1122,7 @@ pub(crate) async fn built_tools(
None
};
let auth = sess.services.auth_manager.auth().await;
let discoverable_tools = if apps_enabled && turn_context.tools_config.tool_suggest {
let discoverable_tools = if apps_enabled && tool_suggest_enabled(turn_context) {
if let Some(accessible_connectors) = accessible_connectors_with_enabled_state.as_ref() {
match connectors::list_tool_suggest_discoverable_tools_with_auth(
&turn_context.config,
@@ -1152,12 +1154,12 @@ pub(crate) async fn built_tools(
&all_mcp_tools,
connectors.as_deref(),
&turn_context.config,
&turn_context.tools_config,
search_tool_enabled(turn_context),
);
let mcp_tools = has_mcp_servers.then_some(mcp_tool_exposure.direct_tools);
let deferred_mcp_tools = mcp_tool_exposure.deferred_tools;
Ok(Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
Ok(Arc::new(ToolRouter::from_turn_context(
turn_context,
ToolRouterParams {
mcp_tools,
deferred_mcp_tools,
+26 -122
View File
@@ -14,10 +14,6 @@ use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManager>) -> bool {
auth_manager.is_some_and(AuthManager::current_auth_uses_codex_backend)
}
#[derive(Clone, Debug)]
pub(crate) struct TurnSkillsContext {
pub(crate) outcome: Arc<SkillLoadOutcome>,
@@ -84,7 +80,9 @@ pub struct TurnContext {
pub(crate) network: Option<NetworkProxy>,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) available_models: Vec<ModelPreset>,
pub(crate) unified_exec_shell_mode: UnifiedExecShellMode,
pub(crate) goal_tools_supported: bool,
pub features: ManagedFeatures,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
pub(crate) final_output_json_schema: Option<Value>,
@@ -156,6 +154,14 @@ impl TurnContext {
self.features.apps_enabled_for_auth(uses_codex_backend)
}
pub(crate) fn tool_environment_mode(&self) -> ToolEnvironmentMode {
ToolEnvironmentMode::from_count(self.environments.turn_environments.len())
}
pub(crate) fn goal_tools_enabled(&self) -> bool {
self.goal_tools_supported && self.features.get().enabled(Feature::Goals)
}
pub(crate) async fn with_model(
&self,
model: String,
@@ -195,61 +201,9 @@ impl TurnContext {
/*developer_instructions*/ None,
);
let features = self.features.clone();
let provider_capabilities = self.provider.capabilities();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await,
features: &features,
image_generation_tool_auth_allowed: image_generation_tool_auth_allowed(
self.auth_manager.as_deref(),
),
web_search_mode: self.tools_config.web_search_mode,
session_source: self.session_source.clone(),
permission_profile: &self.permission_profile,
windows_sandbox_level: self.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_allow_login_shell(self.tools_config.allow_login_shell)
.with_environment_mode(self.tools_config.environment_mode)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(self.tools_config.goal_tools)
.with_max_concurrent_threads_per_session(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_concurrent_threads_per_session),
)
.with_wait_agent_min_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&config.agent_roles,
));
let available_models = models_manager
.list_models(RefreshStrategy::OnlineIfUncached)
.await;
Self {
sub_id: self.sub_id.clone(),
@@ -283,7 +237,9 @@ impl TurnContext {
network: self.network.clone(),
windows_sandbox_level: self.windows_sandbox_level,
shell_environment_policy: self.shell_environment_policy.clone(),
tools_config,
available_models,
unified_exec_shell_mode: self.unified_exec_shell_mode.clone(),
goal_tools_supported: self.goal_tools_supported,
features,
ghost_snapshot: self.ghost_snapshot.clone(),
final_output_json_schema: self.final_output_json_schema.clone(),
@@ -503,72 +459,18 @@ impl Session {
model_info.slug.as_str(),
);
let session_source = session_configuration.session_source.clone();
let image_generation_tool_auth_allowed =
image_generation_tool_auth_allowed(auth_manager.as_deref());
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = create_model_provider(provider, auth_manager);
let provider_capabilities = provider_for_context.capabilities();
let session_telemetry_for_context = session_telemetry;
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager.try_list_models().unwrap_or_default(),
features: &per_turn_config.features,
image_generation_tool_auth_allowed,
web_search_mode: Some(per_turn_config.web_search_mode.value()),
session_source: session_source.clone(),
permission_profile: &session_configuration.permission_profile(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
})
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
.with_image_generation_capability(provider_capabilities.image_generation)
.with_web_search_capability(provider_capabilities.web_search)
.with_unified_exec_shell_mode_for_session(
let available_models = models_manager.try_list_models().unwrap_or_default();
let shell_command_backend =
shell_command_backend_for_features(per_turn_config.features.get());
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
shell_command_backend,
crate::tools::tool_user_shell_type(user_shell),
shell_zsh_path,
main_execve_wrapper_exe,
)
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_environment_mode(ToolEnvironmentMode::from_count(
environments.turn_environments.len(),
))
.with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
.with_multi_agent_v2_tool_namespace(per_turn_config.multi_agent_v2.tool_namespace.clone())
.with_multi_agent_v2_non_code_mode_only(per_turn_config.multi_agent_v2.non_code_mode_only)
.with_goal_tools_allowed(goal_tools_supported)
.with_max_concurrent_threads_per_session(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(
per_turn_config
.multi_agent_v2
.max_concurrent_threads_per_session,
),
)
.with_wait_agent_min_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.min_wait_timeout_ms),
)
.with_wait_agent_max_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.max_wait_timeout_ms),
)
.with_wait_agent_default_timeout_ms(
per_turn_config
.features
.enabled(Feature::MultiAgentV2)
.then_some(per_turn_config.multi_agent_v2.default_wait_timeout_ms),
)
.with_agent_type_description(crate::agent::role::spawn_tool_spec::build(
&per_turn_config.agent_roles,
));
);
let mut per_turn_config = per_turn_config;
per_turn_config.service_tier = per_turn_config
@@ -616,7 +518,9 @@ impl Session {
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
shell_environment_policy: per_turn_config.permissions.shell_environment_policy.clone(),
tools_config,
available_models,
unified_exec_shell_mode,
goal_tools_supported,
features: per_turn_config.features.clone(),
ghost_snapshot: per_turn_config.ghost_snapshot.clone(),
final_output_json_schema: None,
@@ -268,8 +268,8 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
session.services.extensions = Arc::new(builder.build());
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
let router = Arc::new(ToolRouter::from_config(
&turn_context.tools_config,
let router = Arc::new(ToolRouter::from_turn_context(
&turn_context,
crate::tools::router::ToolRouterParams {
mcp_tools: None,
deferred_mcp_tools: None,
@@ -183,7 +183,7 @@ impl ToolExecutor<ToolInvocation> for ShellCommandHandler {
session.as_ref(),
turn.as_ref(),
session.conversation_id,
turn.tools_config.allow_login_shell,
turn.config.permissions.allow_login_shell,
)?;
let shell_type = Some(session.user_shell().shell_type.clone());
run_exec_like(RunExecLikeArgs {
@@ -144,8 +144,8 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
let resolved_command = get_command(
&args,
session.user_shell(),
&turn.tools_config.unified_exec_shell_mode,
turn.tools_config.allow_login_shell,
&turn.unified_exec_shell_mode,
turn.config.permissions.allow_login_shell,
)
.map_err(FunctionCallError::RespondToModel)?;
let command = resolved_command.command;
+1
View File
@@ -14,6 +14,7 @@ pub(crate) mod runtimes;
pub(crate) mod sandboxing;
pub(crate) mod spec_plan;
pub(crate) mod tool_dispatch_trace;
pub(crate) mod tool_family;
pub(crate) mod tool_search_entry;
use std::borrow::Cow;
+9 -2
View File
@@ -287,8 +287,15 @@ impl ToolRegistry {
}
#[cfg(test)]
pub(crate) fn has_tool(&self, name: &ToolName) -> bool {
self.tool(name).is_some()
pub(crate) fn tool_names_for_test(&self) -> Vec<ToolName> {
let mut names = self.tools.keys().cloned().collect::<Vec<_>>();
names.sort();
names
}
#[cfg(test)]
pub(crate) fn tool_exposure(&self, name: &ToolName) -> Option<ToolExposure> {
self.tools.get(name).map(|tool| tool.exposure())
}
pub(crate) fn create_diff_consumer(
+15 -3
View File
@@ -17,7 +17,6 @@ use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio_util::sync::CancellationToken;
@@ -46,8 +45,8 @@ pub(crate) struct ToolRouterParams<'a> {
}
impl ToolRouter {
pub fn from_config(config: &ToolsConfig, params: ToolRouterParams<'_>) -> Self {
build_tool_router(config, params)
pub fn from_turn_context(turn_context: &TurnContext, params: ToolRouterParams<'_>) -> Self {
build_tool_router(turn_context, params)
}
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self {
@@ -61,6 +60,19 @@ impl ToolRouter {
self.model_visible_specs.clone()
}
#[cfg(test)]
pub(crate) fn registered_tool_names_for_test(&self) -> Vec<ToolName> {
self.registry.tool_names_for_test()
}
#[cfg(test)]
pub(crate) fn tool_exposure_for_test(
&self,
name: &ToolName,
) -> Option<crate::tools::registry::ToolExposure> {
self.registry.tool_exposure(name)
}
pub(crate) fn create_diff_consumer(
&self,
tool_name: &ToolName,
+10 -10
View File
@@ -106,8 +106,8 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
.await
.list_all_tools()
.await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: Some(mcp_tools),
@@ -172,8 +172,8 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
#[tokio::test]
async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: Some(vec![
@@ -220,8 +220,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
#[tokio::test]
async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()> {
let (_, turn) = make_session_and_context().await;
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
@@ -272,8 +272,8 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
},
];
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
@@ -328,8 +328,8 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let (mut session, turn) = make_session_and_context().await;
session.services.extensions = extension_tool_test_registry();
let router = ToolRouter::from_config(
&turn.tools_config,
let router = ToolRouter::from_turn_context(
&turn,
ToolRouterParams {
deferred_mcp_tools: None,
mcp_tools: None,
+356 -222
View File
@@ -1,6 +1,4 @@
use crate::config::DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS;
use crate::config::DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS;
use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation;
use crate::tools::handlers::ApplyPatchHandler;
@@ -8,8 +6,6 @@ use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
@@ -19,13 +15,10 @@ use crate::tools::handlers::ReadMcpResourceHandler;
use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::TestSyncHandler;
use crate::tools::handlers::ToolSearchHandler;
use crate::tools::handlers::UpdateGoalHandler;
use crate::tools::handlers::ViewImageHandler;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
use crate::tools::handlers::extension_tools::ExtensionToolAdapter;
@@ -55,9 +48,15 @@ use crate::tools::registry::ToolRegistry;
use crate::tools::registry::override_tool_exposure;
use crate::tools::router::ToolRouter;
use crate::tools::router::ToolRouterParams;
use crate::tools::tool_family::shell::ShellToolsOptions;
use crate::tools::tool_family::shell::build_shell_tools;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
@@ -68,9 +67,12 @@ use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolOutput;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use codex_tools::can_request_original_image_detail;
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
use codex_tools::default_namespace_description;
use codex_tools::request_user_input_available_modes;
use codex_tools::shell_command_backend_for_features;
use codex_tools::shell_type_for_model_and_features;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;
@@ -78,8 +80,34 @@ use tracing::warn;
const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
type PlannedRuntime = Arc<dyn CoreToolRuntime>;
#[derive(Default)]
struct PlannedTools {
runtimes: Vec<PlannedRuntime>,
hosted_specs: Vec<ToolSpec>,
}
impl PlannedTools {
fn add_runtime<T>(&mut self, runtime: T)
where
T: CoreToolRuntime + 'static,
{
self.runtimes.push(Arc::new(runtime));
}
fn add_runtime_arc(&mut self, runtime: PlannedRuntime) {
self.runtimes.push(runtime);
}
fn runtimes(&self) -> &[PlannedRuntime] {
&self.runtimes
}
}
#[derive(Clone, Copy)]
struct ToolRegistryBuildParams<'a> {
struct CoreToolPlanContext<'a> {
turn_context: &'a TurnContext,
mcp_tools: Option<&'a [ToolInfo]>,
deferred_mcp_tools: Option<&'a [ToolInfo]>,
discoverable_tools: Option<&'a [DiscoverableTool]>,
@@ -89,13 +117,16 @@ struct ToolRegistryBuildParams<'a> {
wait_agent_timeouts: WaitAgentTimeoutOptions,
}
pub(crate) fn build_tool_router(config: &ToolsConfig, params: ToolRouterParams<'_>) -> ToolRouter {
let (model_visible_specs, registry) = build_tool_specs_and_registry(config, params);
pub(crate) fn build_tool_router(
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
) -> ToolRouter {
let (model_visible_specs, registry) = build_tool_specs_and_registry(turn_context, params);
ToolRouter::from_parts(registry, model_visible_specs)
}
fn build_tool_specs_and_registry(
config: &ToolsConfig,
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
) -> (Vec<ToolSpec>, ToolRegistry) {
let ToolRouterParams {
@@ -107,65 +138,73 @@ fn build_tool_specs_and_registry(
} = params;
let default_agent_type_description =
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let mut executors = collect_tool_executors(
config,
ToolRegistryBuildParams {
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_executors: &extension_tool_executors,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: wait_agent_timeout_options(config),
},
);
append_tool_search_executor(config, &mut executors);
prepend_code_mode_executors(config, &mut executors);
build_model_visible_specs_and_registry(config, executors, hosted_model_tool_specs(config))
let context = CoreToolPlanContext {
turn_context,
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_executors: &extension_tool_executors,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: wait_agent_timeout_options(turn_context),
};
let mut planned_tools = PlannedTools::default();
add_tool_sources(&context, &mut planned_tools);
append_tool_search_executor(&context, &mut planned_tools);
prepend_code_mode_executors(&context, &mut planned_tools);
build_model_visible_specs_and_registry(turn_context, planned_tools)
}
fn build_model_visible_specs_and_registry(
config: &ToolsConfig,
executors: Vec<Arc<dyn CoreToolRuntime>>,
hosted_specs: Vec<ToolSpec>,
turn_context: &TurnContext,
planned_tools: PlannedTools,
) -> (Vec<ToolSpec>, ToolRegistry) {
let PlannedTools {
runtimes,
hosted_specs,
} = planned_tools;
let mut specs = Vec::new();
let mut seen_tool_names = HashSet::new();
for executor in &executors {
let tool_name = executor.tool_name();
for runtime in &runtimes {
let tool_name = runtime.tool_name();
if !seen_tool_names.insert(tool_name.clone()) {
continue;
}
let exposure = executor.exposure();
let exposure = runtime.exposure();
if exposure.is_direct()
&& !is_hidden_by_code_mode_only(config, &tool_name, exposure)
&& let Some(spec) = executor.spec()
&& !is_hidden_by_code_mode_only(turn_context, &tool_name, exposure)
&& let Some(spec) = runtime.spec()
{
specs.push(spec_for_model_request(config, exposure, spec));
specs.push(spec_for_model_request(turn_context, exposure, spec));
}
}
for spec in hosted_specs {
if !is_hidden_by_code_mode_only(config, &ToolName::plain(spec.name()), ToolExposure::Direct)
{
if !is_hidden_by_code_mode_only(
turn_context,
&ToolName::plain(spec.name()),
ToolExposure::Direct,
) {
specs.push(spec);
}
}
let registry = ToolRegistry::from_tools(executors);
let registry = ToolRegistry::from_tools(runtimes);
let model_visible_specs = merge_into_namespaces(specs)
.into_iter()
.filter(|spec| config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)))
.filter(|spec| {
namespace_tools_enabled(turn_context) || !matches!(spec, ToolSpec::Namespace(_))
})
.collect();
(model_visible_specs, registry)
}
fn spec_for_model_request(
config: &ToolsConfig,
turn_context: &TurnContext,
exposure: ToolExposure,
spec: ToolSpec,
) -> ToolSpec {
if config.code_mode_enabled
if code_mode_enabled(turn_context)
&& exposure != ToolExposure::DirectModelOnly
&& codex_code_mode::is_code_mode_nested_tool(spec.name())
{
@@ -175,33 +214,104 @@ fn spec_for_model_request(
}
}
pub(crate) fn hosted_model_tool_specs(config: &ToolsConfig) -> Vec<ToolSpec> {
pub(crate) fn hosted_model_tool_specs(turn_context: &TurnContext) -> Vec<ToolSpec> {
let mut specs = Vec::new();
let provider_capabilities = turn_context.provider.capabilities();
let web_search_mode = provider_capabilities
.web_search
.then_some(turn_context.config.web_search_mode.value());
let web_search_config = if provider_capabilities.web_search {
turn_context.config.web_search_config.as_ref()
} else {
None
};
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
web_search_mode: config.web_search_mode,
web_search_config: config.web_search_config.as_ref(),
web_search_tool_type: config.web_search_tool_type,
web_search_mode,
web_search_config,
web_search_tool_type: turn_context.model_info.web_search_tool_type,
}) {
specs.push(web_search_tool);
}
if config.image_gen_tool {
if image_generation_tool_enabled(turn_context) {
specs.push(create_image_generation_tool("png"));
}
specs
}
fn wait_agent_timeout_options(config: &ToolsConfig) -> WaitAgentTimeoutOptions {
if config.multi_agent_v2 {
pub(crate) fn search_tool_enabled(turn_context: &TurnContext) -> bool {
turn_context.model_info.supports_search_tool
}
pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool {
let features = turn_context.features.get();
features.enabled(Feature::ToolSuggest)
&& features.enabled(Feature::Apps)
&& features.enabled(Feature::Plugins)
}
fn namespace_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.provider.capabilities().namespace_tools
}
fn code_mode_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::CodeMode)
}
fn code_mode_only_enabled(turn_context: &TurnContext) -> bool {
code_mode_enabled(turn_context) && turn_context.features.get().enabled(Feature::CodeModeOnly)
}
fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::MultiAgentV2)
}
fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
multi_agent_v2_enabled(turn_context) || turn_context.features.get().enabled(Feature::Collab)
}
fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.goal_tools_enabled()
&& !matches!(
turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::Review)
)
}
fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::SpawnCsv)
}
fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool {
agent_jobs_tools_enabled(turn_context)
&& matches!(
&turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::Other(label))
if label.starts_with("agent_job:")
)
}
fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool {
turn_context
.auth_manager
.as_deref()
.is_some_and(AuthManager::current_auth_uses_codex_backend)
&& turn_context.provider.capabilities().image_generation
&& turn_context
.features
.get()
.enabled(Feature::ImageGeneration)
&& turn_context
.model_info
.input_modalities
.contains(&InputModality::Image)
}
fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOptions {
if multi_agent_v2_enabled(turn_context) {
return WaitAgentTimeoutOptions {
default_timeout_ms: config
.wait_agent_default_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_DEFAULT_WAIT_TIMEOUT_MS),
min_timeout_ms: config
.wait_agent_min_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS),
max_timeout_ms: config
.wait_agent_max_timeout_ms
.unwrap_or(DEFAULT_MULTI_AGENT_V2_MAX_WAIT_TIMEOUT_MS),
default_timeout_ms: turn_context.config.multi_agent_v2.default_wait_timeout_ms,
min_timeout_ms: turn_context.config.multi_agent_v2.min_wait_timeout_ms,
max_timeout_ms: turn_context.config.multi_agent_v2.max_wait_timeout_ms,
};
}
@@ -212,20 +322,34 @@ fn wait_agent_timeout_options(config: &ToolsConfig) -> WaitAgentTimeoutOptions {
}
}
fn agent_type_description(config: &ToolsConfig, default_agent_type_description: &str) -> String {
if config.agent_type_description.is_empty() {
fn max_concurrent_threads_per_session(turn_context: &TurnContext) -> Option<usize> {
multi_agent_v2_enabled(turn_context).then_some(
turn_context
.config
.multi_agent_v2
.max_concurrent_threads_per_session,
)
}
fn agent_type_description(
turn_context: &TurnContext,
default_agent_type_description: &str,
) -> String {
let agent_type_description =
crate::agent::role::spawn_tool_spec::build(&turn_context.config.agent_roles);
if agent_type_description.is_empty() {
default_agent_type_description.to_string()
} else {
config.agent_type_description.clone()
agent_type_description
}
}
fn is_hidden_by_code_mode_only(
config: &ToolsConfig,
turn_context: &TurnContext,
tool_name: &ToolName,
exposure: ToolExposure,
) -> bool {
config.code_mode_only_enabled
code_mode_only_enabled(turn_context)
&& exposure != ToolExposure::DirectModelOnly
&& codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name(
tool_name,
@@ -233,11 +357,11 @@ fn is_hidden_by_code_mode_only(
}
fn build_code_mode_executors(
config: &ToolsConfig,
turn_context: &TurnContext,
executors: &[Arc<dyn CoreToolRuntime>],
deferred_tools_available: bool,
) -> Vec<Arc<dyn CoreToolRuntime>> {
if !config.code_mode_enabled {
if !code_mode_enabled(turn_context) {
return vec![];
}
@@ -262,7 +386,7 @@ fn build_code_mode_executors(
create_code_mode_tool(
&enabled_tools,
&namespace_descriptions,
config.code_mode_only_enabled,
code_mode_only_enabled(turn_context),
deferred_tools_available,
),
code_mode_nested_tool_specs,
@@ -339,203 +463,201 @@ fn code_mode_namespace_descriptions(
namespace_descriptions
}
fn collect_tool_executors(
config: &ToolsConfig,
params: ToolRegistryBuildParams<'_>,
) -> Vec<Arc<dyn CoreToolRuntime>> {
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
let mut executors = Vec::<Arc<dyn CoreToolRuntime>>::new();
fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
add_shell_tools(context, planned_tools);
add_mcp_resource_tools(context, planned_tools);
add_core_utility_tools(context, planned_tools);
add_collaboration_tools(context, planned_tools);
add_mcp_runtime_tools(context, planned_tools);
add_dynamic_tools(context, planned_tools);
add_extension_tools(context, planned_tools);
planned_tools
.hosted_specs
.extend(hosted_model_tool_specs(context.turn_context));
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
match &config.shell_type {
ConfigShellToolType::UnifiedExec => {
executors.push(Arc::new(ExecCommandHandler::new(
ExecCommandHandlerOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
include_environment_id,
},
)));
executors.push(Arc::new(WriteStdinHandler));
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand => {
executors.push(Arc::new(ShellCommandHandler::new(
ShellCommandHandlerOptions {
backend_config: config.shell_command_backend,
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
)));
}
}
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let features = turn_context.features.get();
planned_tools
.runtimes
.extend(build_shell_tools(ShellToolsOptions {
shell_type: shell_type_for_model_and_features(&turn_context.model_info, features),
shell_command_backend: shell_command_backend_for_features(features),
environment_mode: turn_context.tool_environment_mode(),
allow_login_shell: turn_context.config.permissions.allow_login_shell,
exec_permission_approvals_enabled: features.enabled(Feature::ExecPermissionApprovals),
}));
}
fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
if context.mcp_tools.is_some() {
planned_tools.add_runtime(ListMcpResourcesHandler);
planned_tools.add_runtime(ListMcpResourceTemplatesHandler);
planned_tools.add_runtime(ReadMcpResourceHandler);
}
}
fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let features = turn_context.features.get();
let environment_mode = turn_context.tool_environment_mode();
planned_tools.add_runtime(PlanHandler);
if goal_tools_enabled(turn_context) {
planned_tools.add_runtime(GetGoalHandler);
planned_tools.add_runtime(CreateGoalHandler);
planned_tools.add_runtime(UpdateGoalHandler);
}
if config.environment_mode.has_environment()
&& config.shell_type != ConfigShellToolType::Disabled
{
match &config.shell_type {
ConfigShellToolType::UnifiedExec => {
executors.push(Arc::new(ShellCommandHandler::from(
config.shell_command_backend,
)));
}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand
| ConfigShellToolType::Disabled => {}
}
planned_tools.add_runtime(RequestUserInputHandler {
available_modes: request_user_input_available_modes(features),
});
if features.enabled(Feature::RequestPermissionsTool) {
planned_tools.add_runtime(RequestPermissionsHandler);
}
if params.mcp_tools.is_some() {
executors.push(Arc::new(ListMcpResourcesHandler));
executors.push(Arc::new(ListMcpResourceTemplatesHandler));
executors.push(Arc::new(ReadMcpResourceHandler));
}
executors.push(Arc::new(PlanHandler));
if config.goal_tools {
executors.push(Arc::new(GetGoalHandler));
executors.push(Arc::new(CreateGoalHandler));
executors.push(Arc::new(UpdateGoalHandler));
}
executors.push(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
if config.request_permissions_tool_enabled {
executors.push(Arc::new(RequestPermissionsHandler));
}
if config.tool_suggest
if tool_suggest_enabled(turn_context)
&& let Some(discoverable_tools) =
params.discoverable_tools.filter(|tools| !tools.is_empty())
context.discoverable_tools.filter(|tools| !tools.is_empty())
{
executors.push(Arc::new(RequestPluginInstallHandler::new(
discoverable_tools,
)));
planned_tools.add_runtime(RequestPluginInstallHandler::new(discoverable_tools));
}
if config.environment_mode.has_environment() && config.apply_patch_tool_type.is_some() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
executors.push(Arc::new(ApplyPatchHandler::new(include_environment_id)));
if environment_mode.has_environment() && turn_context.model_info.apply_patch_tool_type.is_some()
{
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
planned_tools.add_runtime(ApplyPatchHandler::new(include_environment_id));
}
if config
if turn_context
.model_info
.experimental_supported_tools
.iter()
.any(|tool| tool == "test_sync_tool")
{
executors.push(Arc::new(TestSyncHandler));
planned_tools.add_runtime(TestSyncHandler);
}
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
executors.push(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
if environment_mode.has_environment() {
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
planned_tools.add_runtime(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail(
&turn_context.model_info,
),
include_environment_id,
})));
}));
}
}
if config.collab_tools {
if config.multi_agent_v2 {
let exposure = if config.multi_agent_v2_non_code_mode_only {
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
if collab_tools_enabled(turn_context) {
if multi_agent_v2_enabled(turn_context) {
let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
ToolExposure::DirectModelOnly
} else {
ToolExposure::Direct
};
let tool_namespace = config
.namespace_tools
.then_some(config.multi_agent_v2_tool_namespace.as_deref())
let tool_namespace = namespace_tools_enabled(turn_context)
.then_some(turn_context.config.multi_agent_v2.tool_namespace.as_deref())
.flatten();
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
executors.push(multi_agent_v2_handler(
agent_type_description(turn_context, context.default_agent_type_description);
planned_tools.add_runtime_arc(multi_agent_v2_handler(
SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}),
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
SendMessageHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
FollowupTaskHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
WaitAgentHandlerV2::new(params.wait_agent_timeouts),
planned_tools.add_runtime_arc(multi_agent_v2_handler(
WaitAgentHandlerV2::new(context.wait_agent_timeouts),
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
CloseAgentHandlerV2,
exposure,
tool_namespace,
));
executors.push(multi_agent_v2_handler(
planned_tools.add_runtime_arc(multi_agent_v2_handler(
ListAgentsHandlerV2,
exposure,
tool_namespace,
));
} else {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
executors.push(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description(turn_context, context.default_agent_type_description);
planned_tools.add_runtime(SpawnAgentHandler::new(SpawnAgentToolOptions {
available_models: turn_context.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
executors.push(Arc::new(SendInputHandler));
executors.push(Arc::new(ResumeAgentHandler));
executors.push(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
executors.push(Arc::new(CloseAgentHandler));
hide_agent_type_model_reasoning: turn_context
.config
.multi_agent_v2
.hide_spawn_agent_metadata,
include_usage_hint: turn_context.config.multi_agent_v2.usage_hint_enabled,
usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
max_concurrent_threads_per_session: max_concurrent_threads_per_session(
turn_context,
),
}));
planned_tools.add_runtime(SendInputHandler);
planned_tools.add_runtime(ResumeAgentHandler);
planned_tools.add_runtime(WaitAgentHandler::new(context.wait_agent_timeouts));
planned_tools.add_runtime(CloseAgentHandler);
}
}
if config.agent_jobs_tools {
executors.push(Arc::new(SpawnAgentsOnCsvHandler));
if config.agent_jobs_worker_tools {
executors.push(Arc::new(ReportAgentJobResultHandler));
if agent_jobs_tools_enabled(turn_context) {
planned_tools.add_runtime(SpawnAgentsOnCsvHandler);
if agent_jobs_worker_tools_enabled(turn_context) {
planned_tools.add_runtime(ReportAgentJobResultHandler);
}
}
}
if let Some(mcp_tools) = params.mcp_tools {
fn add_mcp_runtime_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
if let Some(mcp_tools) = context.mcp_tools {
for tool in mcp_tools {
executors.push(Arc::new(McpHandler::new(tool.clone())));
planned_tools.add_runtime(McpHandler::new(tool.clone()));
}
}
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
if let Some(deferred_mcp_tools) = context.deferred_mcp_tools {
for tool in deferred_mcp_tools {
executors.push(Arc::new(McpHandler::with_exposure(
planned_tools.add_runtime(McpHandler::with_exposure(
tool.clone(),
ToolExposure::Deferred,
)));
));
}
}
}
for tool in params.dynamic_tools {
let Some(handler) = DynamicToolHandler::new(tool).map(Arc::new) else {
fn add_dynamic_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
for tool in context.dynamic_tools {
let Some(handler) = DynamicToolHandler::new(tool) else {
tracing::error!(
"Failed to convert dynamic tool {:?} to OpenAI tool",
tool.name
@@ -543,23 +665,31 @@ fn collect_tool_executors(
continue;
};
executors.push(handler);
planned_tools.add_runtime(handler);
}
}
append_extension_tool_executors(config, params.extension_tool_executors, &mut executors);
executors
fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
// Extension ToolContributor implementations are resolved into executors
// before planning. Core only adapts those executors into its runtime set.
append_extension_tool_executors(
context.turn_context,
context.extension_tool_executors,
planned_tools,
);
}
fn append_tool_search_executor(
config: &ToolsConfig,
executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
if !(config.search_tool && config.namespace_tools) {
let turn_context = context.turn_context;
if !(search_tool_enabled(turn_context) && namespace_tools_enabled(turn_context)) {
return;
}
let search_infos = executors
let search_infos = planned_tools
.runtimes()
.iter()
.filter(|executor| executor.exposure() == ToolExposure::Deferred)
.filter_map(|executor| executor.search_info())
@@ -568,42 +698,49 @@ fn append_tool_search_executor(
return;
}
executors.push(Arc::new(ToolSearchHandler::new(search_infos)));
planned_tools.add_runtime(ToolSearchHandler::new(search_infos));
}
fn prepend_code_mode_executors(
config: &ToolsConfig,
executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
) {
let deferred_tools_available = config.search_tool
&& executors
let turn_context = context.turn_context;
let deferred_tools_available = search_tool_enabled(turn_context)
&& planned_tools
.runtimes()
.iter()
.any(|executor| executor.exposure() == ToolExposure::Deferred);
let code_mode_executors =
build_code_mode_executors(config, executors, deferred_tools_available);
executors.splice(0..0, code_mode_executors);
let code_mode_executors = build_code_mode_executors(
turn_context,
planned_tools.runtimes(),
deferred_tools_available,
);
planned_tools.runtimes.splice(0..0, code_mode_executors);
}
fn append_extension_tool_executors(
config: &ToolsConfig,
turn_context: &TurnContext,
executors: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
registered_executors: &mut Vec<Arc<dyn CoreToolRuntime>>,
planned_tools: &mut PlannedTools,
) {
if executors.is_empty() {
return;
}
let mut reserved_tool_names = registered_executors
let mut reserved_tool_names = planned_tools
.runtimes()
.iter()
.map(|executor| executor.tool_name())
.collect::<HashSet<_>>();
if config.code_mode_enabled {
if code_mode_enabled(turn_context) {
reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME));
}
if config.search_tool
&& config.namespace_tools
&& registered_executors
if search_tool_enabled(turn_context)
&& namespace_tools_enabled(turn_context)
&& planned_tools
.runtimes()
.iter()
.any(|executor| executor.exposure() == ToolExposure::Deferred)
{
@@ -616,7 +753,7 @@ fn append_extension_tool_executors(
warn!("Skipping extension tool `{tool_name}`: tool already registered");
continue;
}
registered_executors.push(Arc::new(ExtensionToolAdapter::new(executor)));
planned_tools.add_runtime(ExtensionToolAdapter::new(executor));
}
}
@@ -714,9 +851,6 @@ fn code_mode_namespace_name<'a>(
.map(|namespace_description| namespace_description.name.as_str())
}
#[cfg(test)]
#[path = "spec_plan_model_tests.rs"]
mod model_tests;
#[cfg(test)]
#[path = "spec_plan_tests.rs"]
mod tests;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
pub(crate) mod shell;
@@ -0,0 +1,72 @@
use std::sync::Arc;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ShellCommandBackendConfig;
use codex_tools::ToolEnvironmentMode;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::registry::CoreToolRuntime;
#[derive(Clone, Copy, Debug)]
pub(crate) struct ShellToolsOptions {
pub(crate) shell_type: ConfigShellToolType,
pub(crate) shell_command_backend: ShellCommandBackendConfig,
pub(crate) environment_mode: ToolEnvironmentMode,
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
}
pub(crate) fn build_shell_tools(options: ShellToolsOptions) -> Vec<Arc<dyn CoreToolRuntime>> {
let mut runtimes = Vec::new();
if !options.environment_mode.has_environment() {
return runtimes;
}
let include_environment_id = matches!(options.environment_mode, ToolEnvironmentMode::Multiple);
match options.shell_type {
ConfigShellToolType::UnifiedExec => {
add_runtime(
&mut runtimes,
ExecCommandHandler::new(ExecCommandHandlerOptions {
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
include_environment_id,
}),
);
add_runtime(&mut runtimes, WriteStdinHandler);
// Keep the legacy shell tool registered as a hidden runtime while
// unified exec is model-visible.
add_runtime(
&mut runtimes,
ShellCommandHandler::from(options.shell_command_backend),
);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::Default
| ConfigShellToolType::Local
| ConfigShellToolType::ShellCommand => {
add_runtime(
&mut runtimes,
ShellCommandHandler::new(ShellCommandHandlerOptions {
backend_config: options.shell_command_backend,
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
}),
);
}
}
runtimes
}
fn add_runtime<T>(runtimes: &mut Vec<Arc<dyn CoreToolRuntime>>, runtime: T)
where
T: CoreToolRuntime + 'static,
{
runtimes.push(Arc::new(runtime));
}
@@ -1014,10 +1014,8 @@ impl UnifiedExecProcessManager {
local_policy_env,
};
let mut orchestrator = ToolOrchestrator::new();
let mut runtime = UnifiedExecRuntime::new(
self,
context.turn.tools_config.unified_exec_shell_mode.clone(),
);
let mut runtime =
UnifiedExecRuntime::new(self, context.turn.unified_exec_shell_mode.clone());
let file_system_sandbox_policy = context.turn.file_system_sandbox_policy();
let exec_approval_requirement = context
.session
+4 -4
View File
@@ -1,7 +1,7 @@
# codex-tools
`codex-tools` is the shared support crate for building, adapting, planning, and
executing model-visible tool sets outside `codex-core`.
`codex-tools` is the shared support crate for building, adapting, and executing
model-visible tools outside `codex-core`.
Today this crate owns the host-facing tool models and helpers that no longer
need to live in `core/src/tools/spec.rs` or `core/src/client_common.rs`:
@@ -9,8 +9,8 @@ need to live in `core/src/tools/spec.rs` or `core/src/client_common.rs`:
- aggregate host models such as `ToolSpec`, `ConfiguredToolSpec`,
`LoadableToolSpec`, `ResponsesApiNamespace`, and
`ResponsesApiNamespaceTool`
- host config and discovery models used while assembling tool sets, including
`ToolsConfig`, discoverable-tool models, and request-plugin-install helpers
- host discovery models used while assembling tool sets, including
discoverable-tool models and request-plugin-install helpers
- host adapters such as schema sanitization, MCP/dynamic conversion, code-mode
augmentation, and image-detail normalization
- shared executable-tool contracts such as `ToolExecutor`, `ToolCall`, and
+2 -2
View File
@@ -61,11 +61,11 @@ pub use tool_call::ToolCall;
pub use tool_config::ShellCommandBackendConfig;
pub use tool_config::ToolEnvironmentMode;
pub use tool_config::ToolUserShellType;
pub use tool_config::ToolsConfig;
pub use tool_config::ToolsConfigParams;
pub use tool_config::UnifiedExecShellMode;
pub use tool_config::ZshForkConfig;
pub use tool_config::request_user_input_available_modes;
pub use tool_config::shell_command_backend_for_features;
pub use tool_config::shell_type_for_model_and_features;
pub use tool_definition::ToolDefinition;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
+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;
+125 -266
View File
@@ -1,294 +1,153 @@
use super::*;
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationPolicyConfig;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::PathBuf;
fn model_info() -> ModelInfo {
serde_json::from_value(json!({
"slug": "test-model",
"display_name": "Test Model",
"description": null,
"supported_reasoning_levels": [],
"shell_type": "unified_exec",
"visibility": "list",
"supported_in_api": true,
"priority": 1,
"availability_nux": null,
"upgrade": null,
"base_instructions": "base",
"model_messages": null,
"supports_reasoning_summaries": false,
"default_reasoning_summary": "auto",
"support_verbosity": false,
"default_verbosity": null,
"apply_patch_tool_type": null,
"truncation_policy": {
"mode": "bytes",
"limit": 10000
},
"supports_parallel_tool_calls": false,
"supports_image_detail_original": false,
"context_window": null,
"auto_compact_token_limit": null,
"effective_context_window_percent": 95,
"experimental_supported_tools": [],
"input_modalities": ["text", "image"],
"supports_search_tool": false
}))
.expect("deserialize test model")
use super::*;
fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
ModelInfo {
slug: "test-model".to_string(),
display_name: "Test Model".to_string(),
description: None,
default_reasoning_level: None,
supported_reasoning_levels: Vec::new(),
shell_type,
visibility: ModelVisibility::List,
supported_in_api: true,
priority: 0,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
availability_nux: None,
upgrade: None,
base_instructions: String::new(),
model_messages: None,
supports_reasoning_summaries: false,
default_reasoning_summary: Default::default(),
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::tokens(/*limit*/ 1024),
supports_parallel_tool_calls: true,
supports_image_detail_original: false,
context_window: None,
max_context_window: None,
auto_compact_token_limit: None,
effective_context_window_percent: 95,
experimental_supported_tools: Vec::new(),
input_modalities: codex_protocol::openai_models::default_input_modalities(),
used_fallback_model_metadata: false,
supports_search_tool: false,
}
}
#[test]
fn model_provided_unified_exec_requires_feature_flag() {
let model_info = model_info();
fn shell_features() -> Features {
let mut features = Features::with_defaults();
features.enable(Feature::ShellTool);
features.disable(Feature::ShellZshFork);
features.disable(Feature::UnifiedExec);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand);
features
}
#[test]
fn unified_exec_can_be_enabled_for_restricted_token_workspace_write() {
let model_info = model_info();
let mut features = Features::with_defaults();
fn shell_type_is_derived_from_model_and_feature_gates() {
let model = model_with_shell_type(ConfigShellToolType::UnifiedExec);
let mut features = shell_features();
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.enable(Feature::UnifiedExec);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::workspace_write(),
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
});
let expected_shell_type = if codex_utils_pty::conpty_supported() {
let expected_unified_exec = if codex_utils_pty::conpty_supported() {
ConfigShellToolType::UnifiedExec
} else {
ConfigShellToolType::ShellCommand
};
assert_eq!(tools_config.shell_type, expected_shell_type);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
expected_unified_exec
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::ShellCommand
);
features.disable(Feature::ShellTool);
assert_eq!(
shell_type_for_model_and_features(&model, &features),
ConfigShellToolType::Disabled
);
}
#[test]
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::ShellZshFork);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Live),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand);
fn shell_command_backend_requires_both_shell_tool_and_zsh_fork() {
let mut features = shell_features();
assert_eq!(
tools_config.shell_command_backend,
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
);
features.enable(Feature::ShellZshFork);
assert_eq!(
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::ZshFork
);
features.disable(Feature::ShellTool);
assert_eq!(
tools_config.unified_exec_shell_mode,
shell_command_backend_for_features(&features),
ShellCommandBackendConfig::Classic
);
}
#[test]
fn request_user_input_modes_follow_default_mode_feature() {
let mut features = Features::with_defaults();
features.disable(Feature::DefaultModeRequestUserInput);
assert_eq!(
request_user_input_available_modes(&features),
vec![ModeKind::Plan]
);
features.enable(Feature::DefaultModeRequestUserInput);
assert_eq!(
request_user_input_available_modes(&features),
vec![ModeKind::Default, ModeKind::Plan]
);
}
#[test]
fn unified_exec_shell_mode_uses_zsh_fork_only_when_all_inputs_match() {
let exe = std::env::current_exe().expect("current exe path");
let shell = exe.clone();
let mode = UnifiedExecShellMode::for_session(
ShellCommandBackendConfig::ZshFork,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
);
if cfg!(unix) {
assert!(matches!(mode, UnifiedExecShellMode::ZshFork(_)));
} else {
assert_eq!(mode, UnifiedExecShellMode::Direct);
}
assert_eq!(
UnifiedExecShellMode::for_session(
ShellCommandBackendConfig::Classic,
ToolUserShellType::Zsh,
Some(&shell),
Some(&exe),
),
UnifiedExecShellMode::Direct
);
assert_eq!(
tools_config
.with_unified_exec_shell_mode_for_session(
ToolUserShellType::Zsh,
Some(&PathBuf::from(if cfg!(windows) {
r"C:\opt\codex\zsh"
} else {
"/opt/codex/zsh"
})),
Some(&PathBuf::from(if cfg!(windows) {
r"C:\opt\codex\codex-execve-wrapper"
} else {
"/opt/codex/codex-execve-wrapper"
})),
)
.unified_exec_shell_mode,
if cfg!(unix) {
UnifiedExecShellMode::ZshFork(ZshForkConfig {
shell_zsh_path: AbsolutePathBuf::from_absolute_path("/opt/codex/zsh").unwrap(),
main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path(
"/opt/codex/codex-execve-wrapper",
)
.unwrap(),
})
} else {
UnifiedExecShellMode::Direct
}
);
}
#[test]
fn fallback_apply_patch_models_do_not_use_freeform_tool_by_default() {
let model_info = model_info();
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert_eq!(tools_config.apply_patch_tool_type, None);
}
#[test]
fn subagents_keep_request_user_input_config_and_agent_jobs_workers_opt_in_by_label() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::DefaultModeRequestUserInput);
features.enable(Feature::SpawnCsv);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::SubAgent(SubAgentSource::Other(
"agent_job:test".to_string(),
)),
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert_eq!(
tools_config.request_user_input_available_modes,
request_user_input_available_modes(&features)
);
assert!(tools_config.agent_jobs_tools);
assert!(tools_config.agent_jobs_worker_tools);
}
#[test]
fn image_generation_requires_feature_and_supported_model() {
let supported_model_info = model_info();
let mut unsupported_model_info = supported_model_info.clone();
unsupported_model_info.input_modalities = vec![InputModality::Text];
let mut image_generation_disabled_features = Features::with_defaults();
image_generation_disabled_features.disable(Feature::ImageGeneration);
let mut image_generation_features = Features::with_defaults();
image_generation_features.enable(Feature::ImageGeneration);
let available_models = Vec::new();
let default_tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &supported_model_info,
available_models: &available_models,
features: &image_generation_disabled_features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let supported_tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &supported_model_info,
available_models: &available_models,
features: &image_generation_features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let auth_disallowed_tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &supported_model_info,
available_models: &available_models,
features: &image_generation_features,
image_generation_tool_auth_allowed: false,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let unsupported_tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &unsupported_model_info,
available_models: &available_models,
features: &image_generation_features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
assert!(!default_tools_config.image_gen_tool);
assert!(supported_tools_config.image_gen_tool);
assert!(!auth_disallowed_tools_config.image_gen_tool);
assert!(!unsupported_tools_config.image_gen_tool);
}
#[test]
fn provider_capability_methods_disable_provider_bound_tool_surfaces() {
let model_info = model_info();
let features = Features::with_defaults();
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
tools_config.search_tool = true;
tools_config.tool_suggest = true;
tools_config.image_gen_tool = true;
tools_config.namespace_tools = true;
let tools_config = tools_config
.with_namespace_tools_capability(/*namespace_tools*/ false)
.with_image_generation_capability(/*image_generation*/ false)
.with_web_search_capability(/*web_search*/ false);
assert!(tools_config.search_tool);
assert!(tools_config.tool_suggest);
assert!(!tools_config.image_gen_tool);
assert!(!tools_config.namespace_tools);
assert_eq!(tools_config.web_search_mode, None);
}