Skip to content

Commit f8fe96d

Browse files
authored
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.
1 parent cb8b1bb commit f8fe96d

12 files changed

Lines changed: 390 additions & 8 deletions

File tree

codex-rs/core/src/session/review.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub(super) async fn spawn_review_thread(
2525
let _ = review_features.disable(Feature::WebSearchCached);
2626
let review_web_search_mode = WebSearchMode::Disabled;
2727
let goal_tools_supported = !config.ephemeral && parent_turn_context.tools_config.goal_tools;
28+
let provider_capabilities = parent_turn_context.provider.capabilities();
2829
let tools_config = ToolsConfig::new(&ToolsConfigParams {
2930
model_info: &review_model_info,
3031
available_models: &sess
@@ -41,6 +42,9 @@ pub(super) async fn spawn_review_thread(
4142
permission_profile: &parent_turn_context.permission_profile,
4243
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
4344
})
45+
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
46+
.with_image_generation_capability(provider_capabilities.image_generation)
47+
.with_web_search_capability(provider_capabilities.web_search)
4448
.with_unified_exec_shell_mode_for_session(
4549
crate::tools::spec::tool_user_shell_type(sess.services.user_shell.as_ref()),
4650
sess.services.shell_zsh_path.as_ref(),

codex-rs/core/src/session/turn_context.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ impl TurnContext {
173173
/*developer_instructions*/ None,
174174
);
175175
let features = self.features.clone();
176+
let provider_capabilities = self.provider.capabilities();
176177
let tools_config = ToolsConfig::new(&ToolsConfigParams {
177178
model_info: &model_info,
178179
available_models: &models_manager
@@ -187,6 +188,9 @@ impl TurnContext {
187188
permission_profile: &self.permission_profile,
188189
windows_sandbox_level: self.windows_sandbox_level,
189190
})
191+
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
192+
.with_image_generation_capability(provider_capabilities.image_generation)
193+
.with_web_search_capability(provider_capabilities.web_search)
190194
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
191195
.with_web_search_config(self.tools_config.web_search_config.clone())
192196
.with_allow_login_shell(self.tools_config.allow_login_shell)
@@ -448,6 +452,7 @@ impl Session {
448452
image_generation_tool_auth_allowed(auth_manager.as_deref());
449453
let auth_manager_for_context = auth_manager.clone();
450454
let provider_for_context = create_model_provider(provider, auth_manager);
455+
let provider_capabilities = provider_for_context.capabilities();
451456
let session_telemetry_for_context = session_telemetry;
452457
let tools_config = ToolsConfig::new(&ToolsConfigParams {
453458
model_info: &model_info,
@@ -459,6 +464,9 @@ impl Session {
459464
permission_profile: &session_configuration.permission_profile(),
460465
windows_sandbox_level: session_configuration.windows_sandbox_level,
461466
})
467+
.with_namespace_tools_capability(provider_capabilities.namespace_tools)
468+
.with_image_generation_capability(provider_capabilities.image_generation)
469+
.with_web_search_capability(provider_capabilities.web_search)
462470
.with_unified_exec_shell_mode_for_session(
463471
crate::tools::spec::tool_user_shell_type(user_shell),
464472
shell_zsh_path,

codex-rs/core/src/tools/spec.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
107107
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
108108
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
109109
use crate::tools::handlers::unavailable_tool_message;
110-
use crate::tools::tool_search_entry::build_tool_search_entries;
110+
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
111111

112112
let mut builder = ToolRegistryBuilder::new();
113113
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(
170170
});
171171
let deferred_dynamic_tools = dynamic_tools
172172
.iter()
173-
.filter(|tool| tool.defer_loading)
173+
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
174174
.cloned()
175175
.collect::<Vec<_>>();
176176
let mut tool_search_handler = None;
@@ -270,7 +270,8 @@ pub(crate) fn build_specs_with_discoverable_tools(
270270
}
271271
ToolHandlerKind::ToolSearch => {
272272
if tool_search_handler.is_none() {
273-
let entries = build_tool_search_entries(
273+
let entries = build_tool_search_entries_for_config(
274+
config,
274275
deferred_mcp_tools.as_ref(),
275276
&deferred_dynamic_tools,
276277
);

codex-rs/core/src/tools/spec_tests.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use codex_tools::AdditionalProperties;
2020
use codex_tools::ConfiguredToolSpec;
2121
use codex_tools::DiscoverableTool;
2222
use codex_tools::JsonSchema;
23+
use codex_tools::LoadableToolSpec;
2324
use codex_tools::ResponsesApiNamespaceTool;
2425
use codex_tools::ResponsesApiTool;
2526
use codex_tools::ShellCommandBackendConfig;
@@ -40,6 +41,7 @@ use std::collections::BTreeMap;
4041
use std::path::PathBuf;
4142

4243
use super::*;
44+
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
4345

4446
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
4547
rmcp::model::Tool {
@@ -1030,6 +1032,62 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
10301032
assert!(registry.has_handler(&mcp_alias));
10311033
}
10321034

1035+
#[tokio::test]
1036+
async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_disabled() {
1037+
let model_info = search_capable_model_info().await;
1038+
let mut features = Features::with_defaults();
1039+
features.enable(Feature::ToolSearch);
1040+
let available_models = Vec::new();
1041+
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
1042+
model_info: &model_info,
1043+
available_models: &available_models,
1044+
features: &features,
1045+
image_generation_tool_auth_allowed: true,
1046+
web_search_mode: Some(WebSearchMode::Cached),
1047+
session_source: SessionSource::Cli,
1048+
permission_profile: &PermissionProfile::Disabled,
1049+
windows_sandbox_level: WindowsSandboxLevel::Disabled,
1050+
});
1051+
tools_config.namespace_tools = false;
1052+
let mcp_tools = HashMap::from([(
1053+
"mcp__test_server__echo".to_string(),
1054+
mcp_tool_info(mcp_tool(
1055+
"echo",
1056+
"Echo",
1057+
serde_json::json!({"type": "object"}),
1058+
)),
1059+
)]);
1060+
let dynamic_tools = vec![
1061+
DynamicToolSpec {
1062+
namespace: Some("codex_app".to_string()),
1063+
name: "automation_update".to_string(),
1064+
description: "Create or update automations.".to_string(),
1065+
input_schema: serde_json::json!({"type": "object", "properties": {}}),
1066+
defer_loading: true,
1067+
},
1068+
DynamicToolSpec {
1069+
namespace: None,
1070+
name: "plain_dynamic".to_string(),
1071+
description: "Plain dynamic tool.".to_string(),
1072+
input_schema: serde_json::json!({"type": "object", "properties": {}}),
1073+
defer_loading: true,
1074+
},
1075+
];
1076+
1077+
let entries =
1078+
build_tool_search_entries_for_config(&tools_config, Some(&mcp_tools), &dynamic_tools);
1079+
let outputs = entries
1080+
.into_iter()
1081+
.map(|entry| entry.output)
1082+
.collect::<Vec<_>>();
1083+
1084+
assert_eq!(outputs.len(), 1);
1085+
match &outputs[0] {
1086+
LoadableToolSpec::Function(tool) => assert_eq!(tool.name, "plain_dynamic"),
1087+
LoadableToolSpec::Namespace(_) => panic!("namespace tool_search output should be hidden"),
1088+
}
1089+
}
1090+
10331091
#[tokio::test]
10341092
async fn direct_mcp_tools_register_namespaced_handlers() {
10351093
let config = test_config().await;

codex-rs/core/src/tools/tool_search_entry.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use codex_mcp::ToolInfo;
22
use codex_protocol::dynamic_tools::DynamicToolSpec;
33
use codex_tools::LoadableToolSpec;
44
use codex_tools::ToolSearchResultSource;
5+
use codex_tools::ToolsConfig;
56
use codex_tools::dynamic_tool_to_loadable_tool_spec;
67
use codex_tools::tool_search_result_source_to_loadable_tool_spec;
78
use std::collections::HashMap;
@@ -52,6 +53,24 @@ pub(crate) fn build_tool_search_entries(
5253
entries
5354
}
5455

56+
pub(crate) fn build_tool_search_entries_for_config(
57+
config: &ToolsConfig,
58+
mcp_tools: Option<&HashMap<String, ToolInfo>>,
59+
dynamic_tools: &[DynamicToolSpec],
60+
) -> Vec<ToolSearchEntry> {
61+
let mcp_tools = if config.namespace_tools {
62+
mcp_tools
63+
} else {
64+
None
65+
};
66+
let dynamic_tools = dynamic_tools
67+
.iter()
68+
.filter(|tool| config.namespace_tools || tool.namespace.is_none())
69+
.cloned()
70+
.collect::<Vec<_>>();
71+
build_tool_search_entries(mcp_tools, &dynamic_tools)
72+
}
73+
5574
fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json::Error> {
5675
Ok(ToolSearchEntry {
5776
search_text: build_mcp_search_text(info),

codex-rs/model-provider/src/amazon_bedrock/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use codex_protocol::openai_models::ModelsResponse;
2121
use crate::provider::ModelProvider;
2222
use crate::provider::ProviderAccountResult;
2323
use crate::provider::ProviderAccountState;
24+
use crate::provider::ProviderCapabilities;
2425
use auth::resolve_provider_auth;
2526
use auth::resolve_region;
2627
pub(crate) use catalog::static_model_catalog;
@@ -55,6 +56,14 @@ impl ModelProvider for AmazonBedrockModelProvider {
5556
&self.info
5657
}
5758

59+
fn capabilities(&self) -> ProviderCapabilities {
60+
ProviderCapabilities {
61+
namespace_tools: false,
62+
image_generation: false,
63+
web_search: false,
64+
}
65+
}
66+
5867
fn auth_manager(&self) -> Option<Arc<AuthManager>> {
5968
None
6069
}
@@ -116,4 +125,20 @@ mod tests {
116125
"https://bedrock-mantle.eu-central-1.api.aws/v1"
117126
);
118127
}
128+
129+
#[test]
130+
fn capabilities_disable_unsupported_launch_features() {
131+
let provider = AmazonBedrockModelProvider::new(
132+
ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None),
133+
);
134+
135+
assert_eq!(
136+
provider.capabilities(),
137+
ProviderCapabilities {
138+
namespace_tools: false,
139+
image_generation: false,
140+
web_search: false,
141+
}
142+
);
143+
}
119144
}

codex-rs/model-provider/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ pub use provider::ModelProvider;
1313
pub use provider::ProviderAccountError;
1414
pub use provider::ProviderAccountResult;
1515
pub use provider::ProviderAccountState;
16+
pub use provider::ProviderCapabilities;
1617
pub use provider::SharedModelProvider;
1718
pub use provider::create_model_provider;

codex-rs/model-provider/src/provider.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ use crate::auth::auth_manager_for_provider;
1919
use crate::auth::resolve_provider_auth;
2020
use crate::models_endpoint::OpenAiModelsEndpoint;
2121

22+
/// Optional provider-backed features that Codex may expose at runtime.
23+
///
24+
/// These capabilities are a provider-owned upper bound. Callers can disable
25+
/// more functionality through normal config, but should not expose a feature
26+
/// that the active provider marks unsupported here.
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28+
pub struct ProviderCapabilities {
29+
pub namespace_tools: bool,
30+
pub image_generation: bool,
31+
pub web_search: bool,
32+
}
33+
34+
impl Default for ProviderCapabilities {
35+
fn default() -> Self {
36+
Self {
37+
namespace_tools: true,
38+
image_generation: true,
39+
web_search: true,
40+
}
41+
}
42+
}
43+
2244
/// Current app-visible account state for a model provider.
2345
#[derive(Debug, Clone, PartialEq, Eq)]
2446
pub struct ProviderAccountState {
@@ -59,6 +81,11 @@ pub trait ModelProvider: fmt::Debug + Send + Sync {
5981
/// Returns the configured provider metadata.
6082
fn info(&self) -> &ModelProviderInfo;
6183

84+
/// Returns the provider-owned capability upper bounds.
85+
fn capabilities(&self) -> ProviderCapabilities {
86+
ProviderCapabilities::default()
87+
}
88+
6289
/// Returns the provider-scoped auth manager, when this provider uses one.
6390
///
6491
/// TODO(celia-oai): Make auth manager access internal to this crate so callers
@@ -301,6 +328,16 @@ mod tests {
301328
.expect("valid model")
302329
}
303330

331+
#[test]
332+
fn configured_provider_uses_default_capabilities() {
333+
let provider = create_model_provider(
334+
ModelProviderInfo::create_openai_provider(/*base_url*/ None),
335+
/*auth_manager*/ None,
336+
);
337+
338+
assert_eq!(provider.capabilities(), ProviderCapabilities::default());
339+
}
340+
304341
#[test]
305342
fn create_model_provider_builds_command_auth_manager_without_base_manager() {
306343
let provider = create_model_provider(

codex-rs/tools/src/tool_config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub struct ToolsConfig {
9494
pub web_search_tool_type: WebSearchToolType,
9595
pub image_gen_tool: bool,
9696
pub search_tool: bool,
97+
pub namespace_tools: bool,
9798
pub tool_suggest: bool,
9899
pub exec_permission_approvals_enabled: bool,
99100
pub request_permissions_tool_enabled: bool,
@@ -214,6 +215,7 @@ impl ToolsConfig {
214215
web_search_tool_type: model_info.web_search_tool_type,
215216
image_gen_tool: include_image_gen_tool,
216217
search_tool: include_search_tool,
218+
namespace_tools: true,
217219
tool_suggest: include_tool_suggest,
218220
exec_permission_approvals_enabled,
219221
request_permissions_tool_enabled,
@@ -241,6 +243,27 @@ impl ToolsConfig {
241243
self
242244
}
243245

246+
pub fn with_namespace_tools_capability(mut self, namespace_tools: bool) -> Self {
247+
if !namespace_tools {
248+
self.namespace_tools = false;
249+
}
250+
self
251+
}
252+
253+
pub fn with_image_generation_capability(mut self, image_generation: bool) -> Self {
254+
if !image_generation {
255+
self.image_gen_tool = false;
256+
}
257+
self
258+
}
259+
260+
pub fn with_web_search_capability(mut self, web_search: bool) -> Self {
261+
if !web_search {
262+
self.web_search_mode = None;
263+
}
264+
self
265+
}
266+
244267
pub fn with_spawn_agent_usage_hint(mut self, spawn_agent_usage_hint: bool) -> Self {
245268
self.spawn_agent_usage_hint = spawn_agent_usage_hint;
246269
self

codex-rs/tools/src/tool_config_tests.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,35 @@ fn image_generation_requires_feature_and_supported_model() {
231231
assert!(!auth_disallowed_tools_config.image_gen_tool);
232232
assert!(!unsupported_tools_config.image_gen_tool);
233233
}
234+
235+
#[test]
236+
fn provider_capability_methods_disable_provider_bound_tool_surfaces() {
237+
let model_info = model_info();
238+
let features = Features::with_defaults();
239+
let available_models = Vec::new();
240+
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
241+
model_info: &model_info,
242+
available_models: &available_models,
243+
features: &features,
244+
image_generation_tool_auth_allowed: true,
245+
web_search_mode: Some(WebSearchMode::Cached),
246+
session_source: SessionSource::Cli,
247+
permission_profile: &PermissionProfile::Disabled,
248+
windows_sandbox_level: WindowsSandboxLevel::Disabled,
249+
});
250+
tools_config.search_tool = true;
251+
tools_config.tool_suggest = true;
252+
tools_config.image_gen_tool = true;
253+
tools_config.namespace_tools = true;
254+
255+
let tools_config = tools_config
256+
.with_namespace_tools_capability(/*namespace_tools*/ false)
257+
.with_image_generation_capability(/*image_generation*/ false)
258+
.with_web_search_capability(/*web_search*/ false);
259+
260+
assert!(tools_config.search_tool);
261+
assert!(tools_config.tool_suggest);
262+
assert!(!tools_config.image_gen_tool);
263+
assert!(!tools_config.namespace_tools);
264+
assert_eq!(tools_config.web_search_mode, None);
265+
}

0 commit comments

Comments
 (0)