mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: disable capabilities by model provider (#19442)
## Why Unsupported features must fail closed and Codex must not expose OpenAI-hosted fallback paths when the active provider cannot support them. In practice, Bedrock should not surface app connectors, MCP servers, tool search/suggestions, image generation, web search, or JS REPL until those paths are explicitly supported for that provider. This PR moves that decision into provider-owned capability metadata instead of scattering Bedrock-specific checks across callers. ## What changed - Adds `ProviderCapabilities` to `codex-model-provider`, with default support for existing providers and a Bedrock override that disables unsupported launch surfaces. - Adds `ToolCapabilityBounds` to `codex-tools` so provider capability limits can clamp otherwise-enabled tool config. - Applies capability bounds when building session and review-thread tool config. - Routes MCP/app connector configuration through `McpManager::mcp_config`, which filters configured MCP servers and app connectors based on the active provider. - Updates app-server MCP list/read paths to use the filtered MCP config. - Adds coverage for default provider capabilities, Bedrock disabled capabilities, and optional tool-surface clamping. ## Testing built locally and verified that bedrock responses api now return without errors calling unsupported tools.
This commit is contained in:
committed by
GitHub
Unverified
parent
cb8b1bbcd6
commit
f8fe96d548
@@ -25,6 +25,7 @@ pub(super) async fn spawn_review_thread(
|
||||
let _ = review_features.disable(Feature::WebSearchCached);
|
||||
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
|
||||
@@ -41,6 +42,9 @@ pub(super) async fn spawn_review_thread(
|
||||
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(
|
||||
crate::tools::spec::tool_user_shell_type(sess.services.user_shell.as_ref()),
|
||||
sess.services.shell_zsh_path.as_ref(),
|
||||
|
||||
@@ -173,6 +173,7 @@ 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
|
||||
@@ -187,6 +188,9 @@ impl TurnContext {
|
||||
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)
|
||||
@@ -448,6 +452,7 @@ impl Session {
|
||||
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,
|
||||
@@ -459,6 +464,9 @@ impl Session {
|
||||
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(
|
||||
crate::tools::spec::tool_user_shell_type(user_shell),
|
||||
shell_zsh_path,
|
||||
|
||||
@@ -107,7 +107,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
use crate::tools::handlers::unavailable_tool_message;
|
||||
use crate::tools::tool_search_entry::build_tool_search_entries;
|
||||
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
|
||||
|
||||
let mut builder = ToolRegistryBuilder::new();
|
||||
let mcp_tool_plan_inputs = mcp_tools.as_ref().map(map_mcp_tools_for_plan);
|
||||
@@ -170,7 +170,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
});
|
||||
let deferred_dynamic_tools = dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading)
|
||||
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut tool_search_handler = None;
|
||||
@@ -270,7 +270,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
}
|
||||
ToolHandlerKind::ToolSearch => {
|
||||
if tool_search_handler.is_none() {
|
||||
let entries = build_tool_search_entries(
|
||||
let entries = build_tool_search_entries_for_config(
|
||||
config,
|
||||
deferred_mcp_tools.as_ref(),
|
||||
&deferred_dynamic_tools,
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_tools::AdditionalProperties;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ShellCommandBackendConfig;
|
||||
@@ -40,6 +41,7 @@ use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
|
||||
|
||||
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
|
||||
rmcp::model::Tool {
|
||||
@@ -1030,6 +1032,62 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
assert!(registry.has_handler(&mcp_alias));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_disabled() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ToolSearch);
|
||||
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.namespace_tools = false;
|
||||
let mcp_tools = HashMap::from([(
|
||||
"mcp__test_server__echo".to_string(),
|
||||
mcp_tool_info(mcp_tool(
|
||||
"echo",
|
||||
"Echo",
|
||||
serde_json::json!({"type": "object"}),
|
||||
)),
|
||||
)]);
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create or update automations.".to_string(),
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
defer_loading: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "plain_dynamic".to_string(),
|
||||
description: "Plain dynamic tool.".to_string(),
|
||||
input_schema: serde_json::json!({"type": "object", "properties": {}}),
|
||||
defer_loading: true,
|
||||
},
|
||||
];
|
||||
|
||||
let entries =
|
||||
build_tool_search_entries_for_config(&tools_config, Some(&mcp_tools), &dynamic_tools);
|
||||
let outputs = entries
|
||||
.into_iter()
|
||||
.map(|entry| entry.output)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(outputs.len(), 1);
|
||||
match &outputs[0] {
|
||||
LoadableToolSpec::Function(tool) => assert_eq!(tool.name, "plain_dynamic"),
|
||||
LoadableToolSpec::Namespace(_) => panic!("namespace tool_search output should be hidden"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
let config = test_config().await;
|
||||
|
||||
@@ -2,6 +2,7 @@ use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::dynamic_tool_to_loadable_tool_spec;
|
||||
use codex_tools::tool_search_result_source_to_loadable_tool_spec;
|
||||
use std::collections::HashMap;
|
||||
@@ -52,6 +53,24 @@ pub(crate) fn build_tool_search_entries(
|
||||
entries
|
||||
}
|
||||
|
||||
pub(crate) fn build_tool_search_entries_for_config(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<&HashMap<String, ToolInfo>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> Vec<ToolSearchEntry> {
|
||||
let mcp_tools = if config.namespace_tools {
|
||||
mcp_tools
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let dynamic_tools = dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| config.namespace_tools || tool.namespace.is_none())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
build_tool_search_entries(mcp_tools, &dynamic_tools)
|
||||
}
|
||||
|
||||
fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json::Error> {
|
||||
Ok(ToolSearchEntry {
|
||||
search_text: build_mcp_search_text(info),
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::openai_models::ModelsResponse;
|
||||
use crate::provider::ModelProvider;
|
||||
use crate::provider::ProviderAccountResult;
|
||||
use crate::provider::ProviderAccountState;
|
||||
use crate::provider::ProviderCapabilities;
|
||||
use auth::resolve_provider_auth;
|
||||
use auth::resolve_region;
|
||||
pub(crate) use catalog::static_model_catalog;
|
||||
@@ -55,6 +56,14 @@ impl ModelProvider for AmazonBedrockModelProvider {
|
||||
&self.info
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities {
|
||||
namespace_tools: false,
|
||||
image_generation: false,
|
||||
web_search: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
|
||||
None
|
||||
}
|
||||
@@ -116,4 +125,20 @@ mod tests {
|
||||
"https://bedrock-mantle.eu-central-1.api.aws/v1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capabilities_disable_unsupported_launch_features() {
|
||||
let provider = AmazonBedrockModelProvider::new(
|
||||
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
provider.capabilities(),
|
||||
ProviderCapabilities {
|
||||
namespace_tools: false,
|
||||
image_generation: false,
|
||||
web_search: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ pub use provider::ModelProvider;
|
||||
pub use provider::ProviderAccountError;
|
||||
pub use provider::ProviderAccountResult;
|
||||
pub use provider::ProviderAccountState;
|
||||
pub use provider::ProviderCapabilities;
|
||||
pub use provider::SharedModelProvider;
|
||||
pub use provider::create_model_provider;
|
||||
|
||||
@@ -19,6 +19,28 @@ use crate::auth::auth_manager_for_provider;
|
||||
use crate::auth::resolve_provider_auth;
|
||||
use crate::models_endpoint::OpenAiModelsEndpoint;
|
||||
|
||||
/// Optional provider-backed features that Codex may expose at runtime.
|
||||
///
|
||||
/// These capabilities are a provider-owned upper bound. Callers can disable
|
||||
/// more functionality through normal config, but should not expose a feature
|
||||
/// that the active provider marks unsupported here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ProviderCapabilities {
|
||||
pub namespace_tools: bool,
|
||||
pub image_generation: bool,
|
||||
pub web_search: bool,
|
||||
}
|
||||
|
||||
impl Default for ProviderCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
namespace_tools: true,
|
||||
image_generation: true,
|
||||
web_search: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current app-visible account state for a model provider.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProviderAccountState {
|
||||
@@ -59,6 +81,11 @@ pub trait ModelProvider: fmt::Debug + Send + Sync {
|
||||
/// Returns the configured provider metadata.
|
||||
fn info(&self) -> &ModelProviderInfo;
|
||||
|
||||
/// Returns the provider-owned capability upper bounds.
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities::default()
|
||||
}
|
||||
|
||||
/// Returns the provider-scoped auth manager, when this provider uses one.
|
||||
///
|
||||
/// TODO(celia-oai): Make auth manager access internal to this crate so callers
|
||||
@@ -301,6 +328,16 @@ mod tests {
|
||||
.expect("valid model")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_provider_uses_default_capabilities() {
|
||||
let provider = create_model_provider(
|
||||
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
|
||||
/*auth_manager*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(provider.capabilities(), ProviderCapabilities::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_model_provider_builds_command_auth_manager_without_base_manager() {
|
||||
let provider = create_model_provider(
|
||||
|
||||
@@ -94,6 +94,7 @@ pub struct ToolsConfig {
|
||||
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,
|
||||
@@ -214,6 +215,7 @@ impl ToolsConfig {
|
||||
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,
|
||||
@@ -241,6 +243,27 @@ impl ToolsConfig {
|
||||
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
|
||||
|
||||
@@ -231,3 +231,35 @@ fn image_generation_requires_feature_and_supported_model() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -263,14 +263,18 @@ pub fn build_tool_registry_plan(
|
||||
let deferred_dynamic_tools = params
|
||||
.dynamic_tools
|
||||
.iter()
|
||||
.filter(|tool| tool.defer_loading)
|
||||
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
|
||||
.collect::<Vec<_>>();
|
||||
let deferred_mcp_tools_for_search = if config.namespace_tools {
|
||||
params.deferred_mcp_tools
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config.search_tool
|
||||
&& (params.deferred_mcp_tools.is_some() || !deferred_dynamic_tools.is_empty())
|
||||
&& (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty())
|
||||
{
|
||||
let mut search_source_infos = params
|
||||
.deferred_mcp_tools
|
||||
let mut search_source_infos = deferred_mcp_tools_for_search
|
||||
.map(|deferred_mcp_tools| {
|
||||
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
|
||||
ToolSearchSource {
|
||||
@@ -296,7 +300,7 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch);
|
||||
|
||||
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
|
||||
if let Some(deferred_mcp_tools) = deferred_mcp_tools_for_search {
|
||||
for tool in deferred_mcp_tools {
|
||||
plan.register_handler(tool.name.clone(), ToolHandlerKind::Mcp);
|
||||
}
|
||||
@@ -589,6 +593,11 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
}
|
||||
|
||||
if !config.namespace_tools {
|
||||
plan.specs
|
||||
.retain(|configured_tool| !matches!(&configured_tool.spec, ToolSpec::Namespace(_)));
|
||||
}
|
||||
|
||||
plan
|
||||
}
|
||||
|
||||
|
||||
@@ -1143,6 +1143,84 @@ fn test_build_specs_mcp_tools_converted() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_specs_are_hidden_when_namespace_tools_are_disabled() {
|
||||
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.namespace_tools = false;
|
||||
|
||||
let (tools, handlers) = build_specs(
|
||||
&tools_config,
|
||||
Some(HashMap::from([(
|
||||
ToolName::namespaced("mcp__sample__", "echo"),
|
||||
mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})),
|
||||
)])),
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&tools, "mcp__sample__");
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::namespaced("mcp__sample__", "echo"),
|
||||
kind: ToolHandlerKind::Mcp,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespaced_dynamic_specs_are_hidden_when_namespace_tools_are_disabled() {
|
||||
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.namespace_tools = false;
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create or update automations.".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
defer_loading: false,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "plain_dynamic".to_string(),
|
||||
description: "Plain dynamic tool.".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
defer_loading: false,
|
||||
},
|
||||
];
|
||||
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&dynamic_tools,
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&tools, "codex_app");
|
||||
assert_contains_tool_names(&tools, &["plain_dynamic"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_mcp_namespace_description_falls_back_when_missing() {
|
||||
let model_info = model_info();
|
||||
@@ -1398,6 +1476,44 @@ fn search_tool_requires_model_capability_and_enabled_feature() {
|
||||
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_is_hidden_when_only_deferred_namespace_tools_are_available() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ToolSearch);
|
||||
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.namespace_tools = false;
|
||||
|
||||
let (tools, handlers) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
Some(vec![deferred_mcp_tool(
|
||||
"_create_event",
|
||||
"mcp__codex_apps__calendar",
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some("Calendar"),
|
||||
Some("Plan events and manage your calendar."),
|
||||
)]),
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
||||
assert!(!handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::plain(TOOL_SEARCH_TOOL_NAME),
|
||||
kind: ToolHandlerKind::ToolSearch,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_registers_for_deferred_dynamic_tools() {
|
||||
let model_info = search_capable_model_info();
|
||||
@@ -1484,6 +1600,55 @@ fn search_tool_registers_for_deferred_dynamic_tools() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_keeps_plain_deferred_dynamic_tools_when_namespace_tools_are_disabled() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ToolSearch);
|
||||
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.namespace_tools = false;
|
||||
let dynamic_tools = vec![
|
||||
DynamicToolSpec {
|
||||
namespace: Some("codex_app".to_string()),
|
||||
name: "automation_update".to_string(),
|
||||
description: "Create or update automations.".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
defer_loading: true,
|
||||
},
|
||||
DynamicToolSpec {
|
||||
namespace: None,
|
||||
name: "plain_dynamic".to_string(),
|
||||
description: "Plain dynamic tool.".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {}}),
|
||||
defer_loading: true,
|
||||
},
|
||||
];
|
||||
|
||||
let (tools, handlers) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&dynamic_tools,
|
||||
);
|
||||
|
||||
assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME, "plain_dynamic"]);
|
||||
assert_lacks_tool_name(&tools, "codex_app");
|
||||
assert!(handlers.contains(&ToolHandlerSpec {
|
||||
name: ToolName::plain(TOOL_SEARCH_TOOL_NAME),
|
||||
kind: ToolHandlerKind::ToolSearch,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_is_not_registered_without_feature_flag() {
|
||||
let model_info = search_capable_model_info();
|
||||
|
||||
Reference in New Issue
Block a user